I'm trying to plot the angle of an object (let's say it's a weather vane) over time. I want to plot it on a polar coordinate system and have the time points be connected by a path, showing how the angle evolves over time. I simply have a dataframe, with one column being the angle in degrees (numeric) and then the time step when the angle was recorded (integer).
But when I run the below code:
ggplot(df, aes(x = angle.from.ref, y = time.step)) +
coord_polar() +
geom_path() +
geom_point() +
scale_x_continuous(limits = c(0, 360), breaks = seq(0, 360, 45))
I get something that looks like this:
The path created by geom_path()
refuses to cross the 0/360 degree line. If a value of 359 is followed by a value of 1, the path will not create a short link passing across the x=0/360 point. Instead, the path curves back ALL the way around the circle, arriving at x=1 from the other side.
I had hoped using coord_polar()
would have solved this, but clearly not. Is there some way I can tell ggplot
that the values 0 and 360 are adjacent/contiguous?
Ok my implementation is a bit hacky, but it might solve your problem. The idea is to simply implement a version of geom_point() that draws lines instead of points.
First, we'll need to build a ggproto
object that inherits from GeomPoint
and modify the way it draws panels. If you look at GeomPoint$draw_panel
, you'll see that our function is virtually the same, but we're using polylineGrob()
instead of pointsGrob()
.
GeomPolarPath <- ggproto(
"GeomPolarPath", GeomPoint,
draw_panel = function(data, panel_params, coord, na.rm = FALSE){
coords <- coord$transform(data, panel_params)
ggplot2:::ggname(
"geom_polarpath",
polylineGrob(coords$x, coords$y,
gp = grid::gpar(col = alpha(coords$colour, coords$alpha),
fill = alpha(coords$fill, coords$alpha),
fontsize = coords$size * .pt + coords$stroke * .stroke/2,
lwd = coords$stroke * .stroke/2))
)
}
)
Now that we have that, we just need to write the usual function for geoms to accept this in layers. Again, this does the same thing as geom_point()
, but passes GeomPolarPath
instead of GeomPoints
to the layer.
geom_polarpath <- function(mapping = NULL, data = NULL, stat = "identity",
position = "identity", ..., na.rm = FALSE, show.legend = NA,
inherit.aes = TRUE)
{
layer(data = data, mapping = mapping, stat = stat, geom = GeomPolarPath,
position = position, show.legend = show.legend, inherit.aes = inherit.aes,
params = list(na.rm = na.rm, ...))
}
Finally, we can happily plot away all we want (blatantly stealing dww's example data):
ggplot(df, aes(x = angle, y = time.step)) +
coord_polar() +
geom_polarpath() +
geom_point() +
scale_x_continuous(limits = c(0, 360), breaks = seq(0, 360, 45))
And here we go. I've only tested this for this particular plot, so I would expect some bugs and wierdness along the way. Potential downside is that it draws straight lines between points, so it doesn't curve along the angles. Good luck!
EDIT: You might need to load the grid package for this to work.
It may be more straightforward to bypass the crossing-over problem: interpolate at the 360/0 point, and plot each revolution as its own section. Here's how it can work:
library(dplyr)
library(ggplot2)
# sample data
n <- 100
df <- data.frame(
angle.from.ref = seq(0, 800, length.out = n),
time.step = seq(Sys.time(), by = "min", length.out = n)
)
df %>%
interpolate.revolutions() %>%
ggplot(aes(x = angle.from.ref, y = time.step,
group = revolution)) +
geom_line(aes(color = factor(revolution)), size = 1) + # color added for illustration
scale_x_continuous(limits = c(0, 360),
breaks = seq(0, 360, 45)) +
coord_polar()
Code for interpolate.revolutions
function:
interpolate.revolutions <- function(df, threshold = 360){
# where df is a data frame with angle in the first column & radius in the second
res <- df
# add a label variable such that each span of 360 degrees belongs to
# a different revolution
res$revolution <- res[[1]] %/% threshold
# keep only the angle values within [0, 360 degrees]
res[[1]] <- res[[1]] %% threshold
# if there are multiple revolutions (i.e. the path needs to cross the 360/0 threshold),
# calculate interpolated values & add them to the data frame
if(n_distinct(res$revolution) > 1){
split.res <- split(res, res$revolution)
res <- split.res[[1]]
for(i in seq_along(split.res)[-1]){
interp.res <- rbind(res[res[[2]] == max(res[[2]]), ],
split.res[[i]][split.res[[i]][[2]] == min(split.res[[i]][[2]]), ])
interp.res[[2]] <- interp.res[[2]][[1]] +
(threshold - interp.res[[1]][1]) /
(threshold - interp.res[[1]][1] + interp.res[[1]][2]) *
diff(interp.res[[2]])
interp.res[[1]] <- c(threshold, 0)
res <- rbind(res, interp.res, split.res[[i]])
}
}
return(res)
}
This approach can be applied to multiple lines in a plot as well. Just apply the function separately to each line:
# sample data for two lines, for different angle values taken at different time points
df2 <- data.frame(
angle.from.ref = c(seq(0, 800, length.out = 0.75 * n),
seq(0, 1500, length.out = 0.25 * n)),
time.step = c(seq(Sys.time(), by = "min", length.out = 0.75 * n),
seq(Sys.time(), by = "min", length.out = 0.25 * n)),
line = c(rep(1, 0.75*n), rep(2, 0.25*n))
)
df2 %>%
tidyr::nest(-line) %>%
mutate(data = purrr::map(data, interpolate.revolutions)) %>%
tidyr::unnest() %>%
ggplot(aes(x = angle.from.ref, y = time.step,
group = interaction(line, revolution),
color = factor(line))) +
geom_line(size = 1) +
scale_x_continuous(limits = c(0, 360),
breaks = seq(0, 360, 45)) +
coord_polar()
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With