Search code examples
rggplot2linepolar-coordinates

Plotting Curved Lines on Polar Charts


I am trying to create a graph that plots points, labels, and lines that connect the points given a start and end position. Then transform it into a polar chart. I can plot the points, labels, and lines, but my issue is when I transform my chart into polar. I have used both geom_curve and geom_segment.

In using geom_curve I get an error because geom_curve is not implemented for non-linear coordinates. Therefore the furthest I can get is this: Geom_Curve with No Polar

In using geom_segment I get it closer to my desired effect, but it draws the lines along the cirlce's circumfrence, which makes sense given how I pass through the coordinates. Here is a photo: Geom_Segment with Polar

I essentially need a geom_curve for polar coordinates, but I have been unable to find one. I would like the lines on the inside of the circle and curved, there will be some overlap but anyway suggestions it look nice with spacing or something would be welcomed.

Data:

k<-18 
ct<-12
q<-6
x_vector1<-seq(1,k,1) 
x_vector2<-seq(1,3,1) 
x_vector3<-seq(k-2,k,1) 
x_vector<-c(x_vector1,x_vector2,x_vector3)

n<-9 ## sets first level radius 
radius1<-rep(n,k) 
b<-13 ## sets second level radius 
radius2<-rep(b,q) 
radius<-c(radius1,radius2)

name<-c('Alice','Bob','Charlie','D','E','F','G','H','I','J','K','L',
        'M','N','O','Peter','Quin','Roger','Alice2','Bob2','Charlie2',
        'Peter2','Quin2','Roger2') 

dframe<-data.frame(x_vector,radius,name)
dframe$label_radius<-dframe$radius+1 

from<-c('Alice2','Bob','Charlie','D','E','Alice2','Charlie2','Charlie',
        'I','J','K','L','M','N','O','Peter','Quin','Alice') 

to<-c('Alice','Alice','Alice','Alice','Alice','Bob',
      'Bob','Bob','Bob','Charlie','Charlie','Peter',
      'Peter','Quin','Quin','Quin','Roger','Roger') 

amt<-c(3,8,8,8,6,2,2,4,2,4,8,1,10,5,9,5,2,1) 

linethick<-c(0.34,0.91,0.91,0.91,0.68,0.23,0.23,0.45,0.23,0.45,
             0.91,0.11,1.14,0.57,1.02,0.57,0.23,0.11) 

to_x<-c(1,1,1,1,1,2,2,2,2,3,3,16,16,17,17,17,18,18) 

to_rad<-c(9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9) 

from_x<-c(1,2,3,4,5,1,3,3,9,10,11,12,13,14,15,16,17,1) 

from_rad<-c(13,9,9,9,9,13,13,9,9,9,9,9,9,9,9,9,9,9) 

stats<-data.frame(from,to,amt,linethick,to_x,to_rad,from_x,from_rad)


p<-ggplot()+

  geom_point(data=dframe,aes(x=x_vector,y=radius),size=3,shape=19)+
  geom_text(data=dframe,aes(x=x_vector,y=label_radius,label=name))+   
  geom_segment(data=stats,aes(x=from_x,y=from_rad,xend=to_x,yend=to_rad, color=to), ## I need arrows starting at TO and going to FROM. ##
               arrow=arrow(angle=15,ends='first',length=unit(0.03,'npc'), type='closed'))+
     ## transform into polar coordinates   coord_polar(theta='x',start=0,direction=-1)
     ## sets up the scale to display from 0 to 7   scale_y_continuous(limits=c(0,14))+
     ## Used to 'push' the points so all 'k' show up.   expand_limits(x=0) p

Solution

  • As others have commented, you can mimic the desired positions produced by coord_polar() by calculating them yourself, in Cartesian coordinates. I.e.:

    x = radius * cos(theta)
    y = radius * sin(theta)
    # where theta is the angle in radians
    

    Manipulate the 2 data frames:

    dframe2 <- dframe %>%
      mutate(x_vector = as.integer(factor(x_vector))) %>%
      mutate(theta = x_vector / n_distinct(x_vector) * 2 * pi + pi / 2) %>%
      mutate(x = radius * cos(theta),
             y = radius * sin(theta),
             y.label = label_radius * sin(theta),
             name = as.character(name))
    
    stats2 <- stats %>%
      select(from, to, amt, linethick) %>%
      mutate_at(vars(from, to), as.character) %>%
      left_join(dframe2 %>% select(name, x, y), 
                by = c("from" = "name")) %>%
      rename(x.start = x, y.start = y) %>%
      left_join(dframe2 %>% select(name, x, y),
                by = c("to" = "name")) %>%
      rename(x.end = x, y.end = y)
    

    Plot using geom_curve():

    # standardize plot range in all directions
    plot.range <- max(abs(c(dframe2$x, dframe2$y, dframe2$y.label))) * 1.1
    
    p <- dframe2 %>%
      ggplot(aes(x = x, y = y)) +
      geom_point() +
      geom_text(aes(y = y.label, label = name)) +
    
      # use 2 geom_curve() layers with different curvatures, such that all segments align
      # inwards inside the circle
      geom_curve(data = stats2 %>% filter(x.start > 0),
                 aes(x = x.start, y = y.start, 
                     xend = x.end, yend = y.end, 
                     color = to),
                 curvature = -0.3,
                 arrow = arrow(angle=15, ends='first',
                               length=unit(0.03,'npc'),
                               type='closed')) +
      geom_curve(data = stats2 %>% filter(x.start <= 0),
                 aes(x = x.start, y = y.start,
                     xend = x.end, yend = y.end,
                     color = to),
                 curvature = 0.3,
                 arrow = arrow(angle=15, ends='first',
                               length=unit(0.03,'npc'),
                               type='closed')) +
      expand_limits(x = c(-plot.range, plot.range),
                    y = c(-plot.range, plot.range)) +
      coord_equal() +
      theme_void()
    
    p
    

    plot w/o grid lines

    If you want polar grid lines, these can be mimicked as well using geom_spoke() and ggfortify package's geom_circle():

    library(ggforce)
    
    p + 
    
      geom_spoke(data = data.frame(x = 0,
                                   y = 0,
                                   angle = pi * seq(from = 0, 
                                                    to = 2, 
                                                    length.out = 9), # number of spokes + 1
                                   radius = plot.range),
                 aes(x = x, y = y, angle = angle, radius = radius),
                 inherit.aes = FALSE, 
                 color = "grey") +
    
      geom_circle(data = data.frame(x0 = 0, 
                                    y0 = 0, 
                                    r = seq(from = 0, 
                                            to = plot.range, 
                                            length.out = 4)), # number of concentric circles + 1
                  aes(x0 = x0, y0 = y0, r = r), 
                  inherit.aes = FALSE,
                  color = "grey", fill = NA)
    

    (Note: If you really want these pseudo-grid lines, plot them before the other geom layers.)

    plot w grid lines