The goal of the geomtextpath
package is to directly
label line-based plots with text that is able to follow a curved path.
It is an extension of the ggplot2
plotting library. Those
already familiar with ggplot2 and the geom_path()
and
geom_text()
geometries, can easily display a path or some
text.
t <- seq(5, -1, length.out = 1000) * pi
spiral <- data.frame(
x = sin(t) * 1000:1,
y = cos(t) * 1000:1
)
rhyme <- paste(
"Like a circle in a spiral, like a wheel within a wheel,",
"never ending or beginning on an ever spinning reel"
)
p <- ggplot(spiral, aes(x, y)) +
coord_equal(xlim = c(-1000, 1000), ylim = c(-1000, 1000))
p + geom_path() + labs(subtitle = "geom_path()")
p + geom_text(
data = data.frame(x = 0, y = 0),
size = 4, label = rhyme
) + labs(subtitle = "geom_text()")
The geomtextpath
extension follows these ggplot2
conventions and displays the path and the text by projecting the text
along the curve.
All of the line-based geom layers in ggplot2
indicated
below, have text and text-box equivalents in the
geomtextpath
package:
ggplot geom | Text equivalent | Label equivalent |
---|---|---|
geom_path |
geom_textpath |
geom_labelpath |
geom_segment |
geom_textsegment |
geom_labelsegment |
geom_line |
geom_textline |
geom_labelline |
geom_abline |
geom_textabline |
geom_labelabline |
geom_hline |
geom_texthline |
geom_labelhline |
geom_vline |
geom_textvline |
geom_labelvline |
geom_density |
geom_textdensity |
geom_labeldensity |
geom_smooth |
geom_textsmooth |
geom_labelsmooth |
geom_contour |
geom_textcontour |
geom_labelcontour |
geom_density2d |
geom_textdensity2d |
geom_labeldensity2d |
geom_sf |
geom_textsf |
geom_labelsf |
Each of these aims to provide the same functionality as the
equivalent ggplot2 geom, but with the addition of a label
aesthetic that will automatically label the line, even if it follows a
curved path.
Although we made an effort to provide text-path equivalents for
commonly used stat-based geoms, such as geom_density()
or
geom_smooth()
, there is not an equivalent for
every stat-based geom. However, the flexibility of ggplot2
allows that most stats can be combined with any geom. Provided that the
stat provides path-like output, this means that for example
stat_ellipse()
can be combined with
geom_textpath()
by setting geom = "textpath"
or geom = "labelpath"
.
ggplot(iris, aes(Sepal.Width, Sepal.Length, colour = Species)) +
geom_point(alpha = 0.3) +
stat_ellipse(
aes(label = Species),
geom = "textpath", hjust = 0.25,
) +
theme(legend.position = "none")
Alternatively, one could also specify the stat inside
geom_textpath()
or geom_labelpath()
.
The mechanism underlying the text positioning will take account of the plotting window however it is rescaled. This means text continues to stick together and follow the path at the correct angle as the plotting window changes in size or in aspect ratio. This is all done in the background, without the need to call your plotting code again every time the window is resized. For example, here is the same plot drawn with different dimensions:
At the heart of {geomtextpath} is textpathGrob()
. This
is a type of graphical object (known as a “grob”), which is drawn by the
{grid} graphics package on which {ggplot2} is built. Everything that you
see in your plotting device when you draw a ggplot is made up of
grobs.
grob <- textpathGrob(label = "My\nlabel", x = c(0.25, 0.75),
y = c(0.25, 0.75), id = c(1, 1))
grob
#> textpath[GRID.textpath.2013]
When a textpathGrob
is created, it measures the
dimensions of the given text label letter-by-letter with the
{textshaping} package. The measurements are stored, along with the x, y
values of the path and any graphics parameters such as color, linewidth
and font.
grob$textpath$label[[1]]
#> glyph ymin xmin xmid xmax substring y_id
#> 1 M 0.01963976 0.0000000 0.0719401 0.1438802 1 2
#> 2 y 0.01963976 0.1438802 0.1932509 0.2426215 1 2
#> 3 l 0.01963976 0.3426649 0.3657769 0.3888889 1 2
#> 4 a 0.01963976 0.3888889 0.4399957 0.4911024 1 2
#> 5 b 0.01963976 0.4911024 0.5440538 0.5970052 1 2
#> 6 e 0.01963976 0.5970052 0.6483290 0.6996528 1 2
#> 7 l 0.01963976 0.6996528 0.7227648 0.7458767 1 2
When a plot is drawn, or the plotting window is rescaled, the {grid}
package always calls makeContent
for each grob in the
current display device. This makes information about the aspect ratio
and absolute size of the current viewport available for further
calculations to be made on-the-fly if necessary before anything is
actually drawn.
The {geomtextpath} package was possible because the
makeContent
function is generic, meaning that we were able
to create a makeContent.textpath
function. This takes the
letter dimensions stored in the grob, the x, y co-ordinates of the path
stored in the grob, and the device dimensions, applying simple(ish)
trigonometry to work out where each letter should be placed and at what
angle. Once this is known, the path and its associated gap can be
calculated. The actual drawing part is then done by dispatching this
information to the grid functions textGrob
and
linesGrob
, which directly draw the graphical objects on the
plotting device.
We have tried to make any calculations done inside the
makeContent
stage efficient, so that there is not a
noticeable lag in the plot being redrawn when the window is
rescaled.