Curved Text in Polar Co-ordinates

Polar co-ordinates in ggplot2 allow the creation of a variety of circular plots. These include pie charts, donut plots, radar charts, and various circular versions of line, bar and scatter plots. In many circumstances, curved text that follows the polar co-ordinate system can be a clearer and more visually appealing way to label elements of these plots and their axes. However, this is not natively possible in ggplot due to the mechanism of text handling in the underlying grid graphics system.

The textpathGrob at the heart of the geomtextpath package now makes it possible to create curved labels and axis text in polar co-ordinates with a minimum of effort. This vignette gives a brief demonstration of these features. We start by loading the necessary packages.

library(ggplot2)
library(geomtextpath)

Curved axis labels

This package includes a new coord called coord_curvedpolar, which is almost identical to coord_polar, but draws curved axis labels around the circumferential (theta) axis.

The native coord_polar in ggplot produces horizontal axis labels, as we can show in the following example:

df <- data.frame(Temperature = c(4.4, 4.6, 6.3, 8.7, 11.6, 14.1, 15.9, 15.5,
                                 13.1, 9.7, 6.7, 4.3, 3.6, 3.9, 6.4, 9.7, 13.2,
                                 15.8, 18, 17.8, 15.1, 11.2, 7.2, 4.4),
                 City = rep(c("Glasgow", "Amsterdam"), each = 12),
                 Month = factor(rep(month.name, 2), month.name))

p <- ggplot(df, aes(Month, Temperature)) +
      geom_col(aes(fill = City), position = position_dodge(width = 1)) +
      geom_vline(xintercept = 1:13 - 0.5, color = "gray90") +
      geom_hline(yintercept = 0:3 * 5, color = "gray90") +
      scale_fill_manual(values = c("darkorange", "dodgerblue4")) +
      ggtitle("Average monthly temperature in Amsterdam and Glasgow") +
      theme_bw() +
      theme(panel.border = element_blank(),
            axis.text.x = element_text(size = 14),
            axis.title.x = element_blank(),
            panel.grid.major = element_blank())

p + coord_polar()

Whereas, if we want curved axis labels, we can simply substitute coord_curvedpolar in as a replacement for coord_polar

p + coord_curvedpolar()

The usual theme adjustments are available via theme(axis.text.x = element_text())

p + coord_curvedpolar() + 
  theme(axis.text.x = element_text(face = 2, color = "orangered3", hjust = 0))

Note that the text that would normally be upside down is automatically flipped to improve legibility.

Curved labels

It is also straightforward to create other labels that curve appropriately in polar co-ordinates, using geom_textpath.

Straight text paths in Cartesian coordinates become curved in polar coordinates.

df <- data.frame(x = c(1, 1000), y = 1, text = "This is a perfectly flat label")

p <- ggplot(df, aes(x, y, label = text)) +
  geom_textpath(size = 6) +
  ylim(c(0.9, 1.1))
p

p + coord_polar(start = pi)

Similarly, the other geoms that are based on the same underlying mechanism curve in polar co-ordinates. For example, take the following plot in standard co-ordinates with a horizontal and vertical reference line:


p <- ggplot(mtcars, aes(mpg, disp)) + 
  geom_point() + 
  geom_texthline(yintercept = 200, label = "displacement", 
                 hjust = 0, vjust = -0.2, color = "red4") +
  geom_textvline(xintercept = 20, label = "consumption", hjust = 0.8,
                 linetype = 2, vjust = 1.3, color = "blue4")

p
#> Warning in geom_texthline(yintercept = 200, label = "displacement", hjust = 0, : All aesthetics have length 1, but the data has 32 rows.
#> i Did you mean to use `annotate()`?
#> Warning in geom_textvline(xintercept = 20, label = "consumption", hjust = 0.8, : All aesthetics have length 1, but the data has 32 rows.
#> i Did you mean to use `annotate()`?

If we translate to polar co-ordinates, the reference lines transform, so that our horizontal line becomes a circle and our vertical line becomes a radial line. However, we don’t need to change our labels; they will adhere to each path as it transforms without needing any other input from the user:


p + coord_polar()
#> Warning in geom_texthline(yintercept = 200, label = "displacement", hjust = 0, : All aesthetics have length 1, but the data has 32 rows.
#> i Did you mean to use `annotate()`?
#> Warning in geom_textvline(xintercept = 20, label = "consumption", hjust = 0.8, : All aesthetics have length 1, but the data has 32 rows.
#> i Did you mean to use `annotate()`?

This means that with geomtextpath, you are free to design and label plots or diagrams in Cartesian co-ordinates:

df <- data.frame(x1 = c(seq(0, 10/6 * pi, pi/3),
                  seq(0, 10/6 * pi, 2*pi/3)),
           y1 = c(rep(2, 6), rep(-1, 3)),
           x2 = c(seq(0, 10/6 * pi, pi/3)  + pi/3,
                  seq(0, 10/6 * pi, 2*pi/3) + 2*pi/3),
           y2 = c(rep(4, 6), rep(2, 3)),
           group = letters[c(1:6, (1:3) * 2)],
           alpha = c(rep(1, 6), rep(0.4, 3)))

p <- ggplot(df, aes(x1, y1)) +
  geom_rect(aes(xmin = x1, xmax = x2, ymin = y1, ymax = y2, fill = group,
                alpha = alpha),
            color = "white", size = 2) +
  geom_textpath(data = data.frame(x1 = seq(0, 2 * pi, length = 300),
           y1 = rep(0.5, 300),
           label = rep(c("stats", "effects", "polar"), each = 100)),
           aes(label = label), linetype = 0, size = 8,
           upright = TRUE) +
  geom_textpath(data = data.frame(x1 = seq(0, 2 * pi, length = 300),
           y1 = rep(3, 300),
           label = rep(c("density", "smooth", "unique", "organic",
                         "easy to use", "automatic"), 
                       each = 50)),
           aes(label = label), linetype = 0, size = 4.6, color = "white",
           upright = TRUE) +
  scale_y_continuous(limits = c(-5, 4)) +
  scale_x_continuous(limits = c(0, 2*pi)) +
  scale_fill_manual(values = c("deepskyblue3", "deepskyblue4",
                               "green3", "green4","tomato", "tomato2")) +
  scale_alpha_identity() +
  theme_void() +
  theme(legend.position = "none") 
#> Warning: Using `size` aesthetic for lines was deprecated in ggplot2 3.4.0.
#> i Please use `linewidth` instead.
#> This warning is displayed once every 8 hours.
#> Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
#> generated.

p

Which will flip nicely to polar co-ordinates without the need for re-labelling.


p + coord_polar()