Search code examples
rgraphgeom-text

R - nudge only selected values and keep others static with geom_text_repel


I want to use geom_text_repel to have my labels lie as close to the edge of a pie graph as possible unless the percent is under a certain value, in which case the label should be nudged further away and connected with a line. I adapted a solution from Move labels in ggplot2 pie graph but increased the xpos values for groups above the threshold.

library(dplyr)
library(ggplot2)
library(ggrepel)
library(scales)
threshold = 0.05    
age <- data.frame(Age = c("20 - 29", "30 - 39", "40 - 49", "50 - 59", "60 - 69"), count = c(27, 29, 26, 16, 2))
age <- age %>% mutate(percent = count/sum(count),
            cs = rev(cumsum(rev(percent))),
            ypos = percent/2 + lead(cs, 1),
            ypos = ifelse(is.na(ypos), percent/2, ypos),
            xpos = ifelse(percent > threshold, 1.8, 1.3),
            xn = ifelse(percent > threshold, 0, 0.5))
ggplot(age, aes_string(x = 1, y = "percent", fill = "Age")) +
    geom_bar(width = 1 , stat = "identity", colour = "black") +
    geom_text_repel(aes(label = percent(percent, accuracy = 0.1), x = xpos, y = ypos), size = 7.5, nudge_x = age$xn, segment.size = .5, direction = "x", force = 0.5, hjust = 1) +
    coord_polar("y" , start = 0, clip = "off") + 
    theme_minimal() +
    theme(axis.text.x = element_blank(),
          axis.title.x = element_blank(),
          axis.text.y = element_blank(),
          axis.title.y = element_blank(),
          panel.border = element_blank(),
          panel.grid = element_blank(),
          legend.title = element_text(size = 22.5),
          legend.text = element_text(size = 19.5),
          legend.box.margin=margin(c(0,0,0,30))) +
    labs(fill = "Age") +
    scale_fill_manual(values = c("#2B83BA", "#FDAE61", "#FFFF99", "#ABDDA4", "#D7191C"))

enter image description here

The below-threshold values act as expected, but the above-threshold values seem to vary in how far they're located from the edge. I believe two things are at play:

  1. The labels are still being "repelled" despite not being that close to any other labels. This is most evident with the 16.0% label.
  2. The xpos dictates the position of the centre of the label, but since the labels are horizontal, they might cut into the graph if the label's positioning is to close to the horizontal axis.

How can I account for these two issues? Or if there any any other issues I'd appreciate help in identifying them. I would consider the 29.0% label to be good enough, if others could follow that format.


Solution

  • I would offer the following tricks:

    1. To overcome the first issue, use both ofgeom_text_repel() and geom_text() for all data, but show the label in geom_text_repel() only for values less than threshold, and show label in geom_text() only for values more than threshold.

    2. To overcome the second issue, use hjust = 'outward' in geom_text(), and adjust the value of nudge_x both in geom_text() and geom_text_repel().

    3. Use geom_segment() to create lines connecting pie chart areas with the labels.

    Here is the full code:

    library(dplyr)
    library(ggplot2)
    library(ggrepel)
    library(scales)
    threshold = 0.05    
    age <- data.frame(Age = c("20 - 29", "30 - 39", "40 - 49", "50 - 59", "60 - 69"), count = c(27, 29, 26, 16, 2))
    age <- age %>% mutate(percent = count/sum(count),
                          cs = rev(cumsum(rev(percent))),
                          ypos = percent/2 + lead(cs, 1),
                          ypos = ifelse(is.na(ypos), percent/2, ypos),
                          xpos = ifelse(percent > threshold, 1.4, 1.8))
    ggplot(age, aes_string(x = 1, y = "percent", fill = "Age")) +
        geom_bar(width = 1 , stat = "identity", colour = "black") +
        theme_minimal() +
        theme(axis.text.x = element_blank(),
              axis.title.x = element_blank(),
              axis.text.y = element_blank(),
              axis.title.y = element_blank(),
              panel.border = element_blank(),
              panel.grid = element_blank(),
              legend.title = element_text(size = 22.5),
              legend.text = element_text(size = 19.5),
              legend.box.margin=margin(c(0,0,0,30))) +
        labs(fill = "Age") +
        scale_fill_manual(values = c("#2B83BA", "#FDAE61", "#FFFF99", "#ABDDA4", "#D7191C")) + 
        geom_segment(aes(x = ifelse(percent<threshold,1, xpos), xend = xpos, y = ypos, yend = ypos)) + 
        geom_text(aes(x = xpos, y = ypos, label = ifelse(percent>threshold,percent(percent, accuracy = 0.1),"")), hjust = "outward", nudge_x  = 0.2, size = 7.5) + 
        geom_text_repel(aes(x = xpos, y = ypos, label = ifelse(percent<threshold, percent(percent, accuracy = 0.1), "")), nudge_x  = 0.2, size = 7.5)+ 
        coord_polar("y")
    
    

    enter image description here

    I have tried this code for more than one values less than threshold by adjusting nudge_x, and it works. For example:

    library(dplyr)
    library(ggplot2)
    library(ggrepel)
    library(scales)
    threshold = 0.05    
    age <- data.frame(Age = c("20 - 29", "30 - 39", "40 - 49", "50 - 59", "60 - 69"), count = c(50, 44, 1, 2, 3))
    age <- age %>% mutate(percent = count/sum(count),
                          cs = rev(cumsum(rev(percent))),
                          ypos = percent/2 + lead(cs, 1),
                          ypos = ifelse(is.na(ypos), percent/2, ypos),
                          xpos = ifelse(percent > threshold, 1.4, 1.8))
    ggplot(age, aes_string(x = 1, y = "percent", fill = "Age")) +
        geom_bar(width = 1 , stat = "identity", colour = "black") +
        theme_minimal() +
        theme(axis.text.x = element_blank(),
              axis.title.x = element_blank(),
              axis.text.y = element_blank(),
              axis.title.y = element_blank(),
              panel.border = element_blank(),
              panel.grid = element_blank(),
              legend.title = element_text(size = 22.5),
              legend.text = element_text(size = 19.5),
              legend.box.margin=margin(c(0,0,0,30))) +
        labs(fill = "Age") +
        scale_fill_manual(values = c("#2B83BA", "#FDAE61", "#FFFF99", "#ABDDA4", "#D7191C")) + 
        geom_segment(aes(x = ifelse(percent<threshold,1, xpos), xend = xpos, y = ypos, yend = ypos)) + 
        geom_text(aes(x = xpos, y = ypos, label = ifelse(percent>threshold,percent(percent, accuracy = 0.1),"")), hjust = "outward", nudge_x  = 0.2, size = 7.5) + geom_text_repel(aes(x = xpos, y = ypos, label = ifelse(percent<threshold, percent(percent, accuracy = 0.1), "")), nudge_x  = 0.5, size = 7.5)+ 
        coord_polar("y")
    
    

    enter image description here