Angle Correction

A matter of perspective

When you have a data set in which vector angles are specified, it is important to realise that that angle should have some frame of reference. For instance if you specify an angle in an xy-plane (e.g. 30 degrees from North) and then plot it in a plane with an aspect ratio of 1:2 (i.e., values on the y-axis are twice as tall as those on the x-axis), the angle changes. In the plot the original angle of 30 degrees North would appear as 15 degrees due to the aspect ratio.

The same is true is you specify geographical directions in spherical coordinates, angles will change if you plot it in a different projection where the spherical coordinates are squashed to the flat surface of a plot.

This can be illustrated by setting up a spatial grid with vectors that all point Northwards:

north_arrows <-
  expand.grid(
    x = seq(-5, 15, length.out = 10),
    y = seq(89.85, 89.9, length.out = 10)
  ) |>
  sf::st_as_sf(coords = c("x", "y"), crs = 4326) |>
  sf::st_make_grid() |>
  stars::st_as_stars(nx = 10, ny = 10) |>
  dplyr::mutate(angle = 0*(2*pi/360))

Note that if we plot this in WGS84 lon lat projection (i.e., spherical coordinates), all arrows point upwards:

library(ggfields)
library(ggplot2)
theme_set(theme_light())

ggplot() +
  geom_fields(data = north_arrows, aes(angle = angle), radius = 1)
#> Angle correction between 0.00 and 0.00 radials

Now if we plot the same data in UTM (zone 31 N in meters, EPSG code 32631), and do so without correction (red) and with correction (green), you will see what happens:

no_correction <-
    geom_fields(data = north_arrows, aes(angle = angle, col = "no correction"), radius = 1,
              .angle_correction = NULL,
              max_radius = ggplot2::unit(0.7, "cm"))

p <-
  ggplot() +
  theme(legend.position = "top") +
  labs(colour = NULL)

p +
  no_correction +
  geom_fields(data = north_arrows, aes(angle = angle, col = "corrected"), radius = 1,
              max_radius = ggplot2::unit(0.7, "cm")) +
  scale_colour_manual(values = c(`no correction` = "red", corrected = "green")) +
  coord_sf(crs = 32631)
#> Angle correction between -0.19 and 0.12 radials

Remember that in reality (and in this plot) the parallels of latitude converge at the North Pole. If we don’t correct the angles of zero degrees (red), they will still point upwards, but in the projection of the plot that doesn’t make sense (they don’t point towards the North Pole). You can see that after correction, the arrows do point to the North Pole and follow the curved parallels of latitude.

Correction mechanism of ggfield

Default correction

ggfield uses the angle_correction() function to correct angles. If your data has a coordinate reference system (see sf::st_crs()) specified, it will be assumed that angles are specified in relation to the Earth’s spherical lon-lat coordinates. In other cases, it is assumed that the angle is specified in the xy-plane given by the coordinates specified with ggplot2::aes(). In the latter case, angles are only corrected for the aspect ratio of the plot.

The angle_correction() function is passed to the .angle_correction argument of the geom_fields() function. This correction function is called when the plot is rendered. When this argument is set to NULL no correction is applied (see red arrows in plot above).

Custom correction

It is also possible to provide a custom correction function. You just need to define a function that accepts the arguments data, panel_params, and coord. The function should return a modified version of the data.frame data that contains a column named angle_correction. angle_correction should contain numeric radials that should be added to the original angle. Let’s say we want a custom correction function that rotates all angles by 90 degrees (pi/2 in radials), it would look like this:

custom_correct <- function(data, panel_params, coord) {
  data |> dplyr::mutate(angle_correction = pi/2)
}

no_correction[[1]]$geom_params$max_radius <-
  ggplot2::unit(0.3, "cm")

p +
  no_correction +
  geom_fields(data = north_arrows, aes(angle = angle, col = "custom correction"),
              radius = 1, .angle_correction = custom_correct,
              max_radius = ggplot2::unit(0.3, "cm")) +
  scale_colour_manual(values = c(`no correction` = "red",
                                 `custom correction` = "orange"))

Conclusion

The take home message is that angles in your data have been defined from a certain perspective, about which ggfield knows nothing. Although the default angle_correction() function does its best to correct angles in your plot, it is up to you to make sure that these corrections actually make sense.