rggplot2latitude-longitudepolar-coordinatesmetr

In ggplot2 with polar coordinates connect points with geom_path across 0/2*pi with unequally spaced data


I have a very similar question to this question from 2014. However, the solution to that question doesn't work in my case, as I can't convert my angular coordinate data to a factor, as they are not equally spaced.

The case I'm considering is a track on a latitude circle, with varying height. Hence, I have three variables: height (z), as a function of time and longitude, in degrees (from 0 to 360). The track passes the prime meridian (0˚E, GMT line), multiple times, and each time a near complete circle is drawn right around the plot instead of crossing the start/end point.

MWE:

library(ggplot2)

z_t_lon <- data.frame(time = seq.POSIXt(as.POSIXct("2019-04-01"), as.POSIXct("2019-04-15"), by="day"),
                      lon = c(300, 350, 10, 20, 5, 355, 15, 140, 260, 330, 350, 25, 45, 12, 300),
                      z = 1:15 - cos(1:15))  

ggplot(z_t_lon, aes(x=lon, y=zg, col=time)) + 
    coord_polar() + 
    geom_point() +
    geom_path() +
    scale_x_continuous(limits=c(0, 360), breaks=seq(0, 360, by=45))   

How do I get the points to be connecting along the shortest distance in polar space, say from 350 to 10 through 0, rather than all the way around? I've tried using the ggperiodic package and metR::scale_x_longitude() without success.


Solution

  • Unfortunately polar co-ordinates just don't natively handle wrap-around paths.

    To my knowledge, there are two ways to produce the appearance of paths crossing the 0/360 line - either convert your data into polar co-ordinates yourself and plot on a Cartesian co-ordinate system, or calculate all the crossing points over the axis line and create segments that meet at these points. I'll demonstrate the latter here, though it isn't easy.

    First we require a small suite of helper functions:

    cross_lon <- function(x, a) {
    
      if(x == 0) return(a)
      if(x == -1) return(c(a, 0, 360))
      return(c(a, 360, 0))
    }
    
    
    cross_z <- function(x, z1, z2, prop) {
    
      if(x == 0) return(z1)
      if(x == -1) return(c(z1, rep((z2 - z1) * prop + z1, 2)))
      return(c(z1, rep((z2 - z1) * prop + z1, 2)))
    }
    
    
    cross_prop <- function(x, lon1, dif) {
    
      if(x ==  0) return(NA)
      if(x ==  1) return((360 - lon1) / dif)
      return(lon1 / abs(dif))
    }
    
    cross_draw <- function(x) if(x == 0) TRUE else c(TRUE, FALSE, FALSE)
    
    cross_group <- function(x) if(x == 0) 0 else c(0, 0, 1)
    

    Now the wrangling / plotting code itself would look something like this:

    z_t_lon %>%
      mutate(next_point = lead(lon, default = last(lon)),
             next_z = lead(z, default = last(z)),
             diff_lon = next_point - lon,
             min_path = ifelse(abs(diff_lon) > 180, 
                               (abs(diff_lon) - 360) * sign(diff_lon),
                               diff_lon),
             crosses = as.numeric((lon + min_path) > 360) - 
                       as.numeric((lon + min_path) < 0),
             prop = unlist(Map(cross_prop, crosses, lon, min_path))) %>%
      group_by(time) %>%
      summarize(lon = unlist(Map(cross_lon, crosses, lon)),
                z = unlist(Map(cross_z, crosses, z, next_z, prop)),
                point = as.vector(sapply(crosses, cross_draw)),
                group = as.vector(sapply(crosses, cross_group)),
                .groups = "drop") %>%
      mutate(group = cumsum(group)) %>%
      ggplot(aes(lon, z, col = time)) + 
      coord_polar() + 
      geom_path(aes(group = group), size = 1.5) +
      geom_point(data = . %>% filter(point), size = 3, shape = 21, 
                 aes(fill = time), color = "black") +
      scale_x_continuous(limits = c(0, 360), breaks = seq(0, 360, by = 45)) +
      theme_minimal(base_size = 16)
    

    enter image description here