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