---
title: "Themes"
format: html
engine: knitr
vignette: |
  %\VignetteIndexEntry{Tutorial}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r include=FALSE}
knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>",
  # fig.path = "man/figures/README-",
  out.width = "70%",
  fig.width = 8,
  dpi = 300,
  asp = 0.625
)
```

The base R aesthetic tends to divide option. Some people like the
default minimalist look of base R plots and are happy to customize the (many)
graphical parameters that are available to them. Others find base plots ugly
and don't want to spend time endlessly tweaking different parameters. Meanwhile,
the "canvas" drawing system of base R graphics creates its own set of issues. 
Each plot element is drawn according to a fixed placement logic, which means 
that the overall composition can't adjust dynamically according to the elements
that are present or not (including titles). This can lead to awkward whitespace
artifacts unless the user explicitly accounts for them.

Regardless of where you stand in this debate, the **tinyplot** view is that base
R graphics should ideally combine flexibility and ease of use with aesthetically
pleasing end results. One way that we enable this is via _themes_.

## `tinytheme()`

The `tinytheme()` function provides a mechansim for easily changing the look of
your plots to match a variety of pre-defined styles. Behind the scenes, this
works by simultaneously setting a group of graphical parameters to achieve a
particular aesthetic. Let's take a look at the "minimal" theme for example,
which is inspired by a well-known **ggplot2** theme.

```{r}
library(tinyplot)

tinytheme("minimal")

tinyplot(
  Sepal.Length ~ Petal.Length | Species,
  data = iris,
  main = "Title of the plot",
  sub = "A smaller subtitle", 
  cap = "Source: A helpful caption"
)
```

One particular feature that may interest users is the fact that `tinytheme()`
uses some internal ~~magic~~ logic to dynamically adjust plot margins to avoid
whitespace and overlapping elements. For example, notice how the plot region 
here bumps out to accomodate the long horizontal y-axis labels:

```{r}
tinyplot(
  I(Sepal.Length*1e5) ~ Petal.Length | Species,
  data = iris,
  yaxl = ",",  # use comma format for the y-axis labels
  main = "Title of the plot",
  sub = "A smaller subtitle", 
  cap = "Note: The left-margin adjusts to accomodate the long y-axis labels"
)
```

As you may have noticed, the changes made by `tinytheme()` are persistent, and
apply to all subsequent `tinyplot()` calls.

```{r}
tinyplot(mpg ~ hp, data = mtcars, main = "Fuel efficiency vs. horsepower")
```

To reset graphical parameters to factory defaults, call  `tinytheme()` without arguments.

```{r}
tinytheme()
tinyplot(mpg ~ hp, data = mtcars, main = "Fuel efficiency vs. horsepower")
```

### Persistent versus ephemeral themes

Setting a persistent theme for all of your plots is often convenient. But there
are also times where you might prefer an ephemeral theme that only applies to a
single plot (plus any added layers). You can invoke such an ephemeral theme by
calling the `tinyplot(..., theme = <theme>)` argument directly.

```{r}
#| layout-ncol: 2
tinyplot(mpg ~ hp, data = mtcars, main = "Ephemeral theme", theme = "clean")
tinyplot(mpg ~ hp, data = mtcars, main = "Back to the default")
```

One of the advantages of ephemeral themes is that they won't interfere with any
other base graphics calls (e.g, `plot`, `coplot`, etc.) In contrast, the 
persistent hook mechanism of `tinytheme()` will also intercept these other base
plot calls, which may lead to undesirable outcomes unless you reset to the
default behaviour.

### Gallery

We'll use the following running example to demonstrate the full gallery of
built-in **tinyplot** themes.

```{r}
p = function(theme = "default") {
  tinyplot(
    bill_len ~ body_mass | species,
    facet = ~sex,
    data = penguins,
    main = paste0('theme = "', theme, '"'),
    sub = "subtitle",
    cap = "caption",
    yaxl = ",",
    theme = theme
  )
  box("outer", lty = 2)
}
```


:::{.callout-note}
For non-dynamic themes, the `sub` and `cap` arguments are effectively
subsitutes, and so will clash for these cases below. But they achieve guaranteed
separate placement for dynamic themes.
:::

```{r}
p() ## same as "default"
p("basic")
p("dynamic")
p("clean")
p("clean2")
p("classic")
p("bw")
p("linedraw")
p("minimal")
p("ipsum")
p("ipsum2")
p("dark")
p("socviz")
p("broadsheet")
p("nber")
p("web")
p("tufte")
p("float")
p("void")
```

::: {.callout-note}
The specialized `"ridge"` and `"ridge2"` themes are only intended for use with
ridge plot types.
:::

```{r}
p2 = function(theme = "ridge") {
  tinyplot(
    species ~ body_mass | species,
    data = penguins,
    type = "ridge",
    main = paste0('theme = "', theme, '"'),
    sub = "subtitle",
    cap = "caption",
    theme = theme
  )
  box("outer", lty = 2)
}

p2("ridge")
p2("ridge2")
```

Please feel free to make suggestions about themes, or contribute new themes by
[opening a Pull Request on Github.](https://github.com/grantmcdermott/tinyplot)

### Custom themes

Creating custom **tinyplot** themes is easy. In this section, we demonstrate how
to tweak aexisting themes in an _ad hoc_ manner, as well as how to register your
own custom themes for convenient re-use. 

#### _Ad hoc_ customization

The `tinytheme()` function accepts all of the graphical parameters supported by
`(t)par()`. This means that you customize a persistent theme simply by passing
down the relevant parameter arguments. For example:

```{r}
#| layout-ncol: 2
tinytheme(
  "dynamic",
  cex = 1.2, cex.main = 1.5,
  col.axis = "darkred", col.main = "firebrick",
  family = "HersheySans",
  grid = TRUE, grid.col = "thistle",
  pch = 2
)
tinyplot(mpg ~ hp, data = mtcars, main = "Fuel efficiency vs. horsepower")
tinyplot(hp ~ mpg, data = mtcars, main = "Horsepower vs. fuel efficiency")
```

```{r}
# reset
tinytheme()
```

Similarly, you can pass a list object to the `themes` argument to customize
an ephemeral theme for a single plot. Here's a more fancy adaptation that builds
off the "dynamic" theme.

```{r}
tinyplot(
  lat ~ long | depth, data = quakes,
  main = "Earthquakes off Fiji",
  xlab = "Longitutde",
  ylab = "Latitude",
  cap = "Data courtesy of the Harvard PRIM-H project",
  palette = "mako",
  # custom ephemeral theme
  theme = list(
    "dynamic",
    bty = "n",
    bg = "#E0D9D2",
    cex = 1.2, cex.main = 1.5, cex.lab = 1.2,
    col = "#3D3532",
    family = "HersheyScript",
    grid = TRUE, grid.col = "gray95", grid.lwd = 1.5
  )
)
```

#### Registering custom themes

If you find yourself reusing the same custom theme settings across multiple
plots or sessions, you may prefer to _register_ them as a named theme with
`tinytheme_register()`. Once a theme is registered, it works just like a
built-in one. So you can set it persistently with `tinytheme(<yourtheme>)`, or
pass it ephemerally with `tinyplot(..., theme = <yourtheme>)`.

Here's a (possibly ill-advised) example of a "pirate"-inspired theme:

```{r}
# Register a custom "pirate" theme that builds on top of "clean"
tinytheme_register(
  "pirate",
  theme = "clean",
  family = "HersheyScript",
  bg = "#f5e6c8", fg = "#3b2209",
  cex.lab = 1.5, cex.main = 1.5, cex.sub = 1.2,
  col = "#3b2209", col.axis = "#5c3a1e", col.cap = "#7a5230", 
  col.lab = "#3b2209", col.main = "#1a0f04", col.sub = "#7a5230",
  grid = TRUE, grid.col = "#c9a96e", grid.lty = "dotted",
  facet.bg = "#e8d4a8", facet.border = "#5c3a1e",
  pch = 4,
  palette.qualitative = c(
    "#8b0000", "#1a5276", "#196f3d", "#7d6608",
    "#6c3483", "#a04000", "#1b4f72", "#145a32"
  )
)

# Use it ephemerally
tinyplot(
  Sepal.Length ~ Petal.Length | Species, iris,
  main = 'Avast, me hearties!',
  sub  = 'Here be a "pirate" theme',
  cap  = '"x" marks the spot',
  theme = 'pirate'
)
```

As a convenience, `tinytheme()` also accepts a `register` argument that
registers _and_ activates the theme in a single call:

```r
# Equivalent to tinytheme_register("float2", ...) + tinytheme("float2")
tinytheme("float", grid = TRUE, register = "float2")
```

You can use `tinytheme_list()` to see all available themes (both built-in and
registered), and `tinytheme_unregister()` to remove one.

```{r}
tinytheme_unregister("pirate")
```

Note that registered themes are session-scoped: they persist across plots but
disappear when R restarts. To make a custom theme permanently available,
register it in your `.Rprofile`:

```r
# In ~/.Rprofile
if (requireNamespace("tinyplot", quietly = TRUE)) {
  tinyplot::tinytheme_register("my_theme", theme = "clean", grid.col = "pink")
}
```

Similarly, package authors can ship their own custom (tiny)themes as part of
their codebase, which any subsequent `tinyplot()` calls can plug into.  

::: {.callout-tip}
## Font families

Fonts are a suprisingly effective way to add impact and personality to your
plots. While it is perhaps underappreciated, base R actually ships with built-in
support for quite a few font families. In the code chunks above we used a member
of the Hershey font family that comes bundled with the base R distribution (see
[`?Hershey`](https://search.r-project.org/R/refmans/grDevices/html/Hershey.html)).
But this built-in support also extends to other popular LaTex and PDF fonts like
"Palatino", "ComputerModern", "Helvetica", "AvantGarde", etc.
(see [`?pdfFonts`](https://search.r-project.org/R/refmans/grDevices/html/postscriptFonts.html)).

For access to a _much_ wider variety of fonts, you might consider the excellent
**showtext** package
([link](https://cran.r-project.org/package=showtext)).
This package allows you to install _any_ font family from the Google Font
catalog, either on-the-fly or downloaded to your permanent fontbook collection.
It plays very nicely with **tinyplot**.

:::

#### Customization tips

One feature of the `tinytheme()` infrastructure that is especially relevant
to customized themes is how dynamic spacing works. Dynamic themes in
**tinyplot** automatically compute margin positions (`mar`, `mgp`) so that axis
and text elements are well-spaced. Moreover, rather than requiring you to
manually reason about how `mar`, `mgp`, and `tcl` combine to produce certain
spacing, which---trust us---is both confusing and error prone, you can control
the gaps between elements directly through two spacing "primitives":

- `gap.axis`: the gap (in margin lines) between the tick tip and the near edge
  of the tick label. Default `0.2`.
- `gap.lab`: the gap (in margin lines) between the far edge of the tick label
  and the near edge of the axis title. Default `1.0`.

These scale automatically with companion features like `cex.axis` and `cex.lab`,
maintaining constant visible spacing regardless of text size. For example:

```{r}
#| layout-ncol: 2
tinytheme("dynamic", gap.axis = 0, gap.lab = 0.5)
tinyplot(mpg ~ hp, data = mtcars, main = "Tighter axis gaps")
box("outer", lty = 2)

tinytheme("dynamic", gap.axis = 2, gap.lab = 2)
tinyplot(mpg ~ hp, data = mtcars, main = "Looser axis gaps")
box("outer", lty = 2)

tinytheme() # reset
```

Again, this is much more convenient that fiddling with `mgp` values. But you can
always provide `mgp` values if you wish; in which it will take precedence and
the primitives are ignored.

To see the full list of parameters that defines a particular theme, simply
assign them to an object. This can be helpful if you want to explore creating
your own custom theme, or tweak an existing theme.

```{r}
#| eval: false

# parms = tinytheme("clean")   # assigns the theme at the same time
parms = tinyplot:::theme_clean # doesn't assign the theme

# show the list of parameters used in the "clean" theme
parms
```

As a final note about customizing themes, please note that `tinytheme` works by
setting a persistent hook that resets parameters before each new plot. This is
an efficient design choice, but it also means that calling `(t)par(<params>)`
on top of an active theme won't work. This is because the active theme's hook
will override your additional parameter tweaks during the creation of the next
plot. All of which is to emphasize that you should pass any theme customizations
directly to `tinytheme()`:

```r
# This won't work as expected:
tinytheme("clean")
tpar(mar = c(5, 5, 2, 2))  # overwritten by the theme hook
<some plot>

# Do this instead:
tinytheme("clean", mar = c(5, 5, 2, 2))
<some plot>
```

## Manual customization with `tpar()`

_Subtitle: And comparison with `par()`_

Themes are a powerful and convenient way to customize your plots. But they are
not the only game in town. As any base R plotter would tell you, another way
to customize your plots by setting global graphics parameters via
[`par()`](https://search.r-project.org/R/refmans/graphics/html/par.html).
If you prefer this approach, then the good news is that it is fully compatable
with **tinyplot**.^[After all, a tinyplot is just a base plot with added
convenience features. We still use the same **graphics** engine under the hood
and any settings and workflows for `plot()` should (ideally) carry over to
`tinyplot()` too.] However, we recommend that you rather use
[`tpar()`](https://grantmcdermott.com/tinyplot/man/tpar.html),
which is an extended version of `par()` that supports all of the latter's
parameters plus some `tinyplot`-specific upgrades.

Here is a quick example, where we impose several global changes (e.g., change
the font family, use Tufte-style floating axes with a background panel grid,
rotate tick labels, etc.) before drawing the plot.

```{r hershey_plus}
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    = 19             # Larger filled points as default
)

tinyplot(Sepal.Length ~ Petal.Length | Species, data = iris, alpha = 0.5)

# optional: reset to the original parameter settings
tpar(op)
```

Again, this approach should feel very familiar to experience base R plotters.
But we will drive home the point by exploring one final difference between
vanilla `par()` and the enhanced `tpar()` equivalent...

The graphical parameters set by `par()` stay in force as long as a graphical
device stays open. On the other hand, these parameters are reset when the
plotting window is closed or, for example, when executing a new code chunk in a
Quarto notebook.^[The **knitr** package, which provides the rendering engine for
Quarto and R Markdown, also has a `global.par` option to overcome this
limitation. See [here](https://yihui.org/knitr/options/#package-options).]

```{r}
par(col = "red", pch = 4)
tinyplot(mpg ~ hp, data = mtcars)
```
```{r}
tinyplot(wt ~ qsec, data = mtcars)
```

In contrast, graphical parameters set by `tpar()` can persist
across devices and Quarto code chunks thanks to a built-in "hook" mechanism (see
`?setHook`). To enable this persistence, we must invoke the `hook=TRUE`
argument.

```{r}
tpar(col = "red", pch = 4, hook = TRUE)
tinyplot(mpg ~ hp, data = mtcars)
```
```{r}
tinyplot(wt ~ qsec, data = mtcars)
```

```{r}
# reset defaults
tinytheme()
```

(Fun fact: Behind the scenes, `tinytheme(<theme_name>)` is simply passing a list
of parameters to `tpar(..., hook = TRUE)`.)
