rggplot2geometryggforce

Adjust text into slices and color the slices inside a circle using ggplot2 in R


i have some survey data and i have created circles inside circles to represent how a survey is going.I am using ggforce to create the circles but my problem is that the last circle i want to slice it into 3 parts and adjust the text inside the slices. Also i want to color them (the slices) with different colors (group A:red,Group B:green,Group C: yellow.) . How can i do it ? Any help ?

total_participants = 7500  
undelivered = 2500
total_invited = 5000  
total_responses = 2000  
total_unanswered = 1100  
group_c = 900 
group_a = 650  
group_b = 450 
partially_completed = 300  
library(ggplot2)
library(ggforce)

# Define total responses in the pie chart
total_count = group_a + group_b + group_c

# Define proportional angles (in radians)
angle_a = 1.4 * pi * (group_a / total_count)  
angle_b = 8 * pi * (group_b / total_count)  
angle_c = 2 * pi - (angle_a + angle_b)  

# Cumulative angles for the pie slices
angles = c(0, angle_a, angle_a + angle_b, 2 * pi)

# Midpoint angles for text placement
mid_angles = c((angles[1] + angles[2]) / 2,  
               (angles[2] + angles[3]) / 2,  
               (angles[3] + angles[4]) / 2) 

# Pie chart position and radius
pie_center_x = 2.7
pie_center_y = -0.6
pie_radius = 3

# Data frame for the pie chart
pie_data = data.frame(
  category = c(paste0("Group A:\n ", group_a), 
               paste0("Group B: \n", group_b + total_unanswered),
               paste0("Group C: \n", group_c)),
  x_start = pie_center_x,
  y_start = pie_center_y,
  x_end = pie_center_x - pie_radius * cos(angles[2:4]),
  y_end = pie_center_y + pie_radius * sin(angles[2:4]),
  text_x = pie_center_x - (pie_radius * 0.6) * cos(mid_angles),  
  text_y = pie_center_y + (pie_radius * 0.4) * sin(mid_angles)
)

# Bubble chart data
bubble_data = data.frame(
  group = c(paste0("Total Participants: ", total_participants),
            paste0("Invited: ", total_invited),
            paste0("Responded: ", total_unanswered + group_a + group_b + group_c),
            paste0("Not Delivered: ", undelivered),
            paste0("Partial Responses: ", partially_completed)),
  radius = c(7, 5, 3, 0.8, 0.8),
  x0 = c(0, 1, 2.7, -5.4, -1),
  y0 = c(0, 0, -0.6, 1, 1.4)
)

# Final plot
plot_bubble_pie = ggplot() +
  
  # Bubble chart
  ggforce::geom_circle(data = bubble_data, aes(x0 = x0, y0 = y0, r = radius, 
                                               fill = factor(group, group)), 
                       alpha = 1) +
  geom_text(data = bubble_data, aes(x = x0, y = y0 + radius + 0.2, label = group), 
            size = 3.2, fontface = "bold") +
  
  # Pie chart slice lines
  geom_segment(data = pie_data, 
               aes(x = x_start, y = y_start, xend = x_end, yend = y_end, color = category),
               color = "black",
               linewidth = 1) +
  
  # Labels inside pie slices
  geom_label(data = pie_data, 
             aes(x = text_x, y = text_y, label = category), 
             size = 3.2, fontface = "bold", label.size = 0, fill = NA) +
  
  # Custom color mappings
  scale_color_manual(values = c("Group A" = "black", "Group B" = "black", "Group C" = "black"), guide = "none") +
  scale_fill_manual(values = c('#77bca2', '#e1926b', '#a09cc8', "grey", "orange", "indianred", "orange"),
                    guide = 'none') +
  
  coord_equal() +
  theme_void()

# Display the plot
plot_bubble_pie

enter image description here


Solution

  • You won't be able to coerce ggforce to give you circle segments. For this kind of problem I would generally just write little geometric functions to give me circles and circle segments as data frames that I can draw as polygons:

    circlify <- function(x, y, label, n, res = 200) {
      r <- sqrt(n/pi)
      xvals <- r * cos(seq(-pi/2, 3*pi/2, len = res)) + x
      yvals <- r * sin(seq(-pi/2, 3*pi/2, len = res)) + y
      data.frame(x = xvals, y = yvals, label = label, n = n)
    }
    
    circle_section <- function(x, y, label, n, start, end, res = 200) {
      r <- sqrt(n/pi)
      theta_start <- (start / n) * 2 * pi - pi/2
      theta_end <- (end / n) * 2 * pi - pi/2
      xvals <- c(0, r * cos(seq(theta_start, theta_end, len = res)), 0) + x
      yvals <- c(0, r * sin(seq(theta_start, theta_end, len = res)), 0) + y
      data.frame(x = xvals, y = yvals, label = label, n = n)
    }
    

    Now we can write the entire plot including data in a single pipe. Note that unlike your original diagram, the size of each circle and segment is proportional to the actual number within each group. I have also used curved text, the better to fit inside the diagram clearly. The "not delivered" section is actually the whole part outside of the "Invited" circle, so it is not clear why you had an extra little labelled circle there. I have instead put a label here to indicate what that coloured section represents.

    library(tidyverse)
    library(geomtextpath)
    
    rbind(circlify(0, 0, "Total Participants = 7500", 7500),
          circlify(8, 0, "Total Invited = 5000", 5000),
          circlify(18, -5, "Completed = 2000", 2000),
          circlify(-12, 16, " ", 300),
          circle_section(18, -5, "Group C = 900", 2000, 0, 900),
          circle_section(18, -5, "Group A = 650", 2000, 900, 1550),
          circle_section(18, -5, "Group B = 450", 2000, 1550, 2000)) |>
      mutate(label = fct_inorder(label)) %>%
      ggplot(aes(x, y)) +
      geom_polygon(aes(fill = label)) +
      geom_textpath(aes(label = label, vjust = label)) +
      geom_textpath(data = circlify(0, 0, "Not Delivered = 2500", 6250), 
                    aes(label = label), color = NA, textcolour = "black",
                    hjust = 0.77, vjust = -1) +
      geom_textpath(data = circlify(8, 0, "Not Completed = 2700", 4000), 
                    aes(label = label), color = NA, textcolour = "black",
                    hjust = 0.9, vjust = -1) +
      annotate(geom = "text", x = -12, y = 16, 
               label = "Partially\nCompleted\nn = 300") +
      coord_equal() +
      scale_fill_manual(values = c('#77bca2', '#e1926b', '#a09cc8', "orange",
                                   "lightblue", "pink", "palegreen"),
                        guide = 'none') +
      scale_vjust_manual(values = c(1.5, 1.5, 1.5, 1.5, -1.2, -1.2, -1.2)) +
      theme_void()
    

    enter image description here