library(ggplot2)
library(ggdiagram)
library(ggplot2)
library(ggdiagram)
Making a simple diagram with a point-and-click drawing program saves time and can produce good results if handled with care. However, making a complex diagram worthy of publication can take many hours of fuss and bother.
When I discovered TikZ, I was amazed at how much better my diagrams could be. TikZ knows things about the objects it draws—like where lines and objects intersect and how far they are from each other. Instead of eyeballing where to place objects, I could tell TikZ exactly where to draw objects in relation to each other (e.g., draw a circle to the right of rectangle with 4cm of space between them). I suspect that the precision, consistency, and beauty of TikZ diagrams add to their persuasive power because readers intuit that such figures are the product of careful deliberation.
Because I mainly compute statistical models in R, getting information from R to TikZ was an intensive process, and integrating TikZ graphics with R graphics was often an exercise in frustration. Though I very much respect the labor of those who have made the TikZ ecosystem open and flexible, I never felt at ease when trying to automate TikZ or extending its capabilities.
When the S7 package became available, it seemed that what I wanted was within my grasp: to draw diagrams with an object-oriented approach, welding the precision of TikZ with the flexibility of R. The ggdiagram package makes objects that can hold or compute various properties and can be placed directly in a ggplot2 plot. I do not imagine that ggdiagram will ever duplicate the full functionality of TikZ, but being able to make TikZ-like diagrams within R eliminates many pain points from my previous workflow and offers a world of possibilities.
The ggplot2 ecosystem already has the ability to create points, lines, and shapes, and it is ideally suited for the display of data and statistical trends. There is no reason to use ggdiagram for the tasks that ggplot2 already does fabulously well. However, once ggplot2 creates points, lines, and shapes, it is not always easy to extract information about them to create new objects. For example, if I draw a line segment from the center of an ellipse to the center of a rectangle, where does the line intersect with the ellipse and the rectangle? Where is the point midway between those intersection points? These quantities can be computed with a bit of algebra and trigonometry, but when making complex figures, such computations are tedious distractions.
The functions in ggdiagram with the ob_
prefix (e.g,. ob_point
, ob_line
, and ob_circle
) create objects using S7, which allows the objects to hold information about the object’s properties (e.g., location, color, and rotation angle) and to compute the location of its edges and points of intersection with other objects. Properties of S7 objects are extracted with the @
operator. For example, we can create a circle with the ob_circle
function and extract various properties:
<- ob_circle()
x @radius
x#> [1] 1
@circumference
x#> [1] 6.283185
@area
x#> [1] 3.141593
@diameter
x#> [1] 2
As seen in Figure 1, objects with the ob_
prefix can be added to any ggplot object in the usual manner.
# Plot
ggplot() +
ob_circle(radius = 1) +
ob_rectangle(width = 2,
height = 2) +
coord_equal() +
theme_void()
ggplot
function
The ggdiagram
function calls the ggplot
function, sets the ggplot2 theme (defaults to theme_void
), and also sets the defaults of key geoms so that font families, font sizes, line widths, and point sizes do not have to be specified repeatedly.
In Figure 2, we can locate points on a circle’s circumference at any angle. The degree
function makes it easy to compute and label angles as degrees.
<- degree(c(0, 53, 90, 138, 180, 241, 270, 338))
theta
ggdiagram(font_size = 18) +
+
x @center +
x@point_at(theta)@label(theta)
x#> Found litedown! Enabling r-universe template
Objects in ggdiagram can be styled in any way that its underlying geom can be styled. Let’s create a segment and its two endpoints.
<- ob_point(-3, 2)
p1 <- ob_point(1, 5)
p2 <- ob_segment(p1, p2)
s1 <- ggdiagram(theme_function = ggplot2::theme_minimal,
bp font_size = 18)
+
bp +
p1 +
p2 s1
The primary options for styling a segment are alpha, color, linetype, and linewidth. However, it can take any style from ggarrow::geom_arrow_segment
If you are not sure which properties can be set, you an see them in the @aesthetics@style
slot.
@aesthetics@style
s1#> [1] "alpha" "arrow_head" "arrow_fins" "arrowhead_length"
#> [5] "color" "length_head" "length_fins" "lineend"
#> [9] "linejoin" "linewidth" "linewidth_fins" "linewidth_head"
#> [13] "linetype" "resect" "resect_fins" "resect_head"
#> [17] "stroke_color" "stroke_width"
Styles can be specified when the object is created.
<- ob_segment(p1, p2, color = "green4")
s2 + s2 bp
Styles can be modified after the segment is created:
@linewidth <- 3
s2+ s2 bp
The as.geom
function passes style arguments to the ggarrow::geom_arrow_segment
function without modifying the segment’s style property:
+
bp as.geom(s1, color = "red4")
as.geom
function
As an alternative, the geom
property is a function that calls as.geom
.
+ s1@geom(color = "blue3") bp
geom
property
To verify that s1 has not changed its color:
+ s1 bp
s1
object has not changed
A “pipe-friendly” way to modify any ggdiagram object is to use S7’s set_props
function, which has been re-exported to ggdiagram for the sake of convenience. Like as.geom
, this function does not modify s1
, but unlike as.geom
, set_props
can be used to save a new object with the specified modifications by assigning it to a new variable. That is, as.geom
creates a ggplot2 geom, whereas set_props
will create a modified a ggdiagram object (or any other S7 class).
+
bp |> set_props(color = "red") s1
The place
function will place an object a specified distance and direction from another object. Here we place a 1 × 1 rectangle below unit circle x
such that the separation between them is .5 units. The connect
function draws an arrow between them, with 2mm of space resected.
<- ob_circle()
x <- ob_rectangle() %>%
y place(from = x, where = "below", sep = .5)
ggdiagram() +
+
x +
y connect(x,y, resect = 2)
The where
parameter can be specified with
degree
function (e.g., degree(21)
)radian
function (e.g., radian(pi)
)turn
function (e.g., turn(1/5)
)Sometimes it is useful to make arrays of objects. The ob_array
function makes k
copies of an object and arranges them along a direct and separates them by a specified amount. By default, the array is arranged horizontally, centered on the original object, separated by 1 unit. Here we make an array of 5 circles.
ggdiagram(font_size = 18) +
ob_circle() %>%
ob_array(k = 5, label = 1:5)
Arrays can go in any direction:
ggdiagram(font_size = 18) +
ob_rectangle() %>%
ob_array(k = 6,
sep = .2,
where = degree(-45),
label = 1:6)
Arrays can be anchored at any point on the rectangle that contains the array. For example, here we create a vertical array of 3 ellipses anchored at the bottom of the bounding box that contains the array.
ggdiagram(font_size = 18, theme_function = ggplot2::theme_minimal) +
ob_ellipse(a = 2) %>%
ob_array(k = 3,
sep = .2,
where = "north",
label = 1:3,
anchor = "south") +
ob_point()
Often we want to build a diagram in which newer objects depend on previously specified objects. To assign a variable in the middle of a ggplot2 workflow, we can enclose an assignment statement in curly braces {}
.
Figure 13 makes use of a curly braces to create:
# Loadings
<- c(.86, .79, .90)
loadings
ggdiagram(font_size = 16) +
# Latent variable
<- ob_circle(radius = 2, label = ob_label("A", size = 96))} +
{A # Observed variables (array of 3 superellipses below A)
<- ob_ellipse(m1 = 20) %>%
{A_3 place(from = A, where = "below", sep = 3) %>%
ob_array(
k = 3,
sep = .5,
label = ob_label(
label = paste0("A~", 1:3, "~"),
size = 32,
vjust = .6
)+
)} # Observed variable loadings
connect(
from = A,
to = A_3,
resect = 2,
label = ob_label(round_probability(loadings),
angle = 0)
+
) # Error variances
ob_variance(
A_3,where = "south",
bend = -15,
label = ob_label(round_probability(sqrt(1 - loadings ^ 2)))
)