3 min read

Drawing an annotated unit circle with ggplot2

The unit circle: everybody’s favorite circle.

I recently needed to an annotated unit circle for some teaching material I was preparing. Rather than using one of the countless pictures already available, I thought it was a good excuse to play around a bit with using mathematical annotations in ggplot2. This post explains the process.

Here’s what we’ll be working towards:

We start by defining a function that, given a radius, generates a data frame of coordinates of a circle centered around the origin.

library(tidyverse)
library(scales)
library(stringr)

get_circle_coords <- function(r = 1, ...) {
  data_frame(theta = seq(0, 2 * pi, ...),
             x     = cos(theta) * r,
             y     = sin(theta) * r)
}

circ <- get_circle_coords(length.out = 200)
qplot(circ$x, circ$y, geom = "path")

Next, we want to generate the coordinates where we go around the unit circle by one-sixth \(\pi\) at each step (we drop the last observation to avoid overlap):

coords_pi <- get_circle_coords(by = pi / 6) %>% head(-1)

We can now plot the circle itself as a geom_path, the hubs as geom_point, and spokes as geom_segment. theme_void drops all unnecessary chart junk, and coord_equal makes sure that one unit on the x-axis is equivalent to one unit on the y-axis.

ggplot(coords_pi, aes(x = x, y = y)) +
  geom_path(data = circ, color = "grey50") +
  geom_point(color = "grey50") +
  geom_segment(aes(xend = 0, yend = 0), color = "grey50") +
  xlim(-1.2, 1.2) +
  ylim(-1.2, 1.2) +
  theme_void() +
  coord_equal()

We add a character variable that gives the equivalent angles in degrees:

coords_pi$angle <- seq(0, 330, 30) %>% paste(" * degree")

To properly typeset the fractions of \(\pi\) we use R’s built-in support for mathematical annotation (?grDevices::plotmath). Since we only need a few annotations, we can hard-code these.1

coords_pi$pi <- c(
  "0", "frac(pi, 6)", "frac(pi, 3)", "frac(pi, 2)", 
  "frac(2 * pi, 3)", "frac(5 * pi, 6)", "pi",
  "frac(7 * pi, 6)", "frac(4 * pi, 3)", "frac(3 * pi, 2)", 
  "frac(5 * pi, 3)", "frac(11 * pi, 6)"
)

Finally, we use some more plotmath to typeset the coordinates of some of the extact trigonometric constants. This might look a bit fiddly, but we only need to figure out the pattern for the first quadrant, and then just make sure we get the signs right for the other quadrants.

coords_pi$trig <- c(
  "1*','* 0",
  "frac(sqrt(3), 2) *','* ~ frac(1,2)",
  "frac(1, 2) *','* ~ frac(sqrt(3), 2)",
  "0*','* 1",
  "-frac(1, 2) *','* ~ frac(sqrt(3), 2)",
  "-frac(sqrt(3), 2) *','* ~ frac(1,2)",
  "-1*','* 0",
  "-frac(sqrt(3), 2) *','* ~ -frac(1,2)",
  "-frac(1, 2) *','* ~ -frac(sqrt(3), 2)",
  "0*','* -1",
  "frac(1, 2) *','* ~ -frac(sqrt(3), 2)",
  "frac(sqrt(3), 2) *','* ~ -frac(1,2)"
)

As pointed out by Rob Creel in a comment, we can also use the bgroup expression to make sure that fractions are enclosed by scalable parentheses. To avoid making the already messy hard-coded string even messier, we define a helper function for this.

bgroup_ <- function(x) {
  sprintf("bgroup('(', %s, ')')", x)
}

coords_pi$trig <- bgroup_(coords_pi$trig)

Since we’re going to plot several layers of geom_label we can use purrr::partial to partially apply all the common arguments that these will take:

geom_l <- partial(geom_label, size = 2.5, 
                  label.size = NA, parse = TRUE, 
                  color = "firebrick")

Lastly, we plot the final illustration:

ggplot(coords_pi, aes(x = x, y = y)) +
  geom_path(data = circ, color = "grey50") +
  geom_point(color = "grey50") +
  geom_segment(aes(xend = 0, yend = 0), color = "grey50") +
  geom_l(aes(label = angle, x = x / 2, y = y / 2)) +
  geom_l(aes(label = pi, x = x * 4/5, y = y * 4/5)) +
  geom_l(aes(label = trig), fill = NA, 
         vjust = "outward", hjust = "outward") +
  xlim(-1.2, 1.2) +
  ylim(-1.2, 1.2) +
  theme_void() +
  coord_equal()


  1. Commenter Centerleft spotted a typo in an earlier version of this code.