Skip to content

Conversation

teunbrand
Copy link
Collaborator

@teunbrand teunbrand commented Jun 25, 2023

This PR fixes #4815, fixes #5059, fixes #3959, and fixes #4462

Briefly, this PR introduces a new coord system coord_polar2() with expanded options (name of function and arguments are up for discussion).

Less briefly, this PR started out with a simpler goal of having coord_polar() use the axis system. Realising that this couldn't be done without breaking backward compatibility, this has instead become coord_polar2(). This PR then started accumulating additional features and here we are today. I think it might be best to explain this PR with a visual walkthrough.

If we use coord_polar2() plainly, we see it resembles coord_polar() with the following differences:

  • Theta is represented by a proper position axis. Figuring out text placement here was the hardest part, and still isn't perfect. The default is to display text of the theta axis horizontally.
  • The panel.background theme setting is applied to the circle that forms the background. You can still use panel.border = element_rect(fill = NA) in the theme if you need a frame for the plot.
  • Radius axis line starts and stops at the actual radius limits and doesn't cover the entire left side of the rectangular panel.
  • The coords has a expand = TRUE default argument so that the behaviour of scale expansion is more in line with coord_cartesian() than with coord_polar().
devtools::load_all("~/packages/ggplot2/")
#> ℹ Loading ggplot2

p <- ggplot(mtcars, aes(disp, mpg)) +
  geom_point() +
  theme(axis.line = element_line())

p + coord_polar2()

One feature is that we can also place the radius axis inside the circle. Admittedly, this looks a little bit awkward because it intersects with the theta axis, which is why this isn't the default when you have a full circle.

p + coord_polar2(r_axis_inside = TRUE)

The next feature is that requested in #4462, namely to have partial polar coordinates. The following things are of note:

  • The start and end arguments to control which sector of a circle the plot occupies.
  • Since we don't have a full circle, the radius axis is placed inside and not at the rectangular bounding box. You can use coord_polar2(r_axis_inside = FALSE) to prevent this.
  • The radius axis snaps to the start position. While the axis itself is rotated to reflect this, the labels remain horizontal.
p + coord_polar2(start = -0.25 * pi, end = 0.25 * pi)

  • If you swap direction, the radius axis follows the swap.
p + coord_polar2(start = -0.25 * pi, end = 0.25 * pi, direction = -1)

Then the reason I started this PR: #3959. A few comments:

  • There is a new guide_axis_theta() that can be used for the theta axis.
  • Radius axes cannot be the new theta guide, which is what prompts the warning below. The inverse is also true, i.e. guides(theta = "axis") also throws a warning.
  • If the angle argument is set, either in a radius axis or theta axis, this is interpreted as a relative angle. Note that the theta text follows the curvature and that the radius text are projected from the tick marks.
p + coord_polar2(start = -0.25 * pi, end = 0.25 * pi) +
  guides(
    r     = guide_axis_theta(),
    r.sec = guide_axis(angle = 0),
    theta = guide_axis_theta(angle = 0)
  )
#> Warning: `guide_axis_theta()` cannot be used for r.
#> ℹ Use one of x, y, or theta instead.

I then took a little bit of a liberty and decided on my own it would be fun to also be able to set a donut hole in polar coordinates. This is mostly just a convenience that doesn't force you to fiddle with the scale limits or expansions to not have all shapes disappear in a single point at the center. Also, it is the only circumstance I could ever imagine that you'd need a secondary theta axis.

p + coord_polar2(donut = 0.5) +
  guides(theta.sec = "axis_theta")

Lastly, I also implemented #5059, since getting the (relative) angle right in text geoms in polar coordinates is a bit of pain. Text between 90 and 270 degrees is flipped for readability reasons.

df <- data.frame(
  x = LETTERS[1:5], lab = c("cat", "farm", "banana", "airplane", "baker")
)

ggplot(df, aes(x, label = lab)) +
  geom_text(aes(y = "0 degrees"),  angle = 0)  +
  geom_text(aes(y = "90 degrees"), angle = 90) +
  coord_polar2(rotate_angle = TRUE)

Created on 2023-06-25 with reprex v2.0.2

A few closing remarks:

  • guide_axis_theta() also works reasonably well with cartesian coordinates.
  • guide_axis_theta() does not respond to hjust/vjust settings, mostly because text placement had given me enough headaches at this point and the theme's defaults translate horribly to the theta axis.
  • The theta positions uses x.bottom theme settings, theta.sec uses x.top, r/r.sec use y.left/y.right depending on the direction argument. In theory, we could change this, but it would involve some hairy adaptations to guide_axis() as well.
  • Clipping currently isn't ideal, as it applies to the rectangular bounding box. We could use R4.1+ features to clip to the curved bounding boxes, but we'd need to implement Feature request: graphics device capabilities checker #5332 for that.

GuideAxisTheta <- ggproto(
"GuideAxisTheta", GuideAxis,

# TODO: delete if minor ticks PR (#5287) gets merged
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This may be simplified once #5287 is merged.

#' # can also be used to add a duplicate guide
#' p + guides(x = guide_axis(n.dodge = 2), y.sec = guide_axis())
guide_axis <- function(title = waiver(), check.overlap = FALSE, angle = NULL,
guide_axis <- function(title = waiver(), check.overlap = FALSE, angle = waiver(),
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default is now waiver() because coord_polar2() may overrule angle for display purposes, but NULL still uses the theme's angle.

}

# it is not worth the effort to align upside-down labels properly
check_number_decimal(angle, min = -90, max = 90)
Copy link
Collaborator Author

@teunbrand teunbrand Jun 25, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I very much did need some alignments for upside-down labels, so I have expanded this function to include this. Due to this, some angles in the svg snapshots have changed from e.g. 45 degrees to -315 degrees but that doesn't matter visually.

position <- params[[1]]$position %||% scale$position
if (position != scale$position) {
order <- rev(order)
if (!is.null(params)) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Layout doesn't know about theta/r guides, so had to avoid taking labels from NULL params here that are returned when the x/y guide doesn't exist.

@teunbrand
Copy link
Collaborator Author

Also worth pointing out that interactions with e.g. {ggtext} aren't that horrible:

devtools::load_all("~/packages/ggplot2/")
#> ℹ Loading ggplot2
library(ggtext)
labels <- c(
  setosa = "<img src='https://upload.wikimedia.org/wikipedia/commons/thumb/8/86/Iris_setosa.JPG/180px-Iris_setosa.JPG'
    width='100' /><br>*I. setosa*",
  virginica = "<img src='https://upload.wikimedia.org/wikipedia/commons/thumb/3/38/Iris_virginica_-_NRCS.jpg/320px-Iris_virginica_-_NRCS.jpg'
    width='100' /><br>*I. virginica*",
  versicolor = "<img src='https://upload.wikimedia.org/wikipedia/commons/thumb/2/27/20140427Iris_versicolor1.jpg/320px-20140427Iris_versicolor1.jpg'
    width='100' /><br>*I. versicolor*"
)

ggplot(iris, aes(Species, Sepal.Width)) +
  geom_boxplot() +
  scale_x_discrete(
    name = NULL,
    labels = labels
  ) +
  coord_polar2(r_axis_inside = TRUE) +
  theme(
    axis.text.x = element_markdown(color = "black", size = 11),
    axis.title.y = element_blank(),
    plot.margin = margin(5,5,70,5)
  )

Created on 2023-06-25 with reprex v2.0.2

Note that you couldn't do this with regular coord_polar().

@teunbrand teunbrand mentioned this pull request Jun 25, 2023
@teunbrand teunbrand added coord 🗺️ feature a feature request or enhancement labels Jul 9, 2023
@thomasp85
Copy link
Member

Name suggestion: coord_radial()

@teunbrand
Copy link
Collaborator Author

Latest changes:

  • Renamed coord_polar2() to coord_radial().
  • Use minor ticks plumbing from Minor ticks #5287, so custom extract_key method and params became redundant.
  • coord_polar() and coord_radial() now share the rescale helper functions

Should be ready for review :)

@teunbrand teunbrand changed the title TLC for polar coordinates TLC for polar coordinates: coord_radial() Oct 25, 2023
@teunbrand
Copy link
Collaborator Author

I couldn't really use Map() with old R (3.6.3) units to render labels, so instead I just vectorised rotate_just(). This allows element_text() to have multiple angles, thereby eliminating the need for Map()ing the labels.

@teunbrand teunbrand added this to the ggplot2 3.5.0 milestone Nov 7, 2023
Copy link
Member

@thomasp85 thomasp85 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I approve this, but please fix the small style stuff I commented on

Comment on lines 87 to 88
# We likely have a linear coord, so we match the text angles to
# standard axes.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if this axis is used with a non-radial coord?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It will draw a very normal looking axis with slightly inferior options for text justification.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

devtools::load_all("~/packages/ggplot2")
#> ℹ Loading ggplot2

ggplot(mpg, aes(displ, hwy)) +
  geom_point() +
  guides(x = "axis_theta", y = "axis_theta") +
  theme(axis.line = element_line())

Created on 2023-11-20 with reprex v2.0.2

@teunbrand teunbrand merged commit 5e29f33 into tidyverse:main Nov 20, 2023
@teunbrand
Copy link
Collaborator Author

Thanks for the review Thomas!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

coord 🗺️ feature a feature request or enhancement

Projects

None yet

2 participants