---
layout: post
title: "How to draw braces to annotate a bar plot in R using ggplot2 (CC346)"
blurb: "Recreating Plate 9"
author: "PD Schloss"
date: 2025-03-03 8:00
comments: false
youtube: 5jmEHIsF89E
start_hash: 
end_hash: 
---

Pat uses R to show how to create braces that are used to annotate dodged bar plots in order to recreate an iconic bar plot figure that WEB DuBois presented at the 1900 Paris Exposition showing the age distribution of Black Georgians relative to the French population using the ggplot2, dplyr, and showtext packages. The functions he uses from these packages include aes, case_when, coord_cartesian, cos, element_blank, element_text, factor, font_add, function, geom_col, geom_path, geom_text, ggplot, ggsave, labs, library, map, margin, paste0, pivot_longer, position_dodge, rev, scale_color_manual, scale_fill_manual, seq, showtext_auto, showtext_opts, sin, theme, tibble, tribble, unit, and unnest. Pat described his strategy for this figure in [his newsletter](https://shop.riffomonas.org/posts/em-bracing-ggplot2-to-recreate-historic-data-visualizations). The book Pat mentions by Whitney Battle-Baptiste and Britt Rusert, titled "W.E.B. Du Bois's Data Portraits: Visualizing Black America" is [available at Amazon](https://amzn.to/4heJOWZ). A great set of talks about the DuBois data portraits is [available here](https://www.youtube.com/watch?v=ZZST1AZj-dQ&ab_channel=Tableau). The Anthony Starks GitHub repository can be [found here](https://github.com/ajstarks/dubois-data-portraits/). If you have a figure that you would like to see me discuss in a future newsletter and episode of Code Club, email me at pat@riffomonas.org!

<iframe style="margin: 0 auto;display:block;" width="560" height="315" src="https://www.youtube.com/embed/{{ page.youtube }}" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

## Code

```R
library(tidyverse)
library(showtext)

font_add("b52", "B52-ULC W00 ULC.ttf")
font_add("vasarely", "vasarely-light.otf")
showtext_auto()
showtext_opts(dpi = 300)

draw_brace <- function(center_x, center_y,
                       aspect_ratio, height, fatten_factor) {
  
  radius <- height * aspect_ratio
  top_shoulder <- seq(pi/2, pi, length.out = 25)
  top_nose <- seq(0, -pi/2, length.out = 25)
  bottom_nose <- seq(pi/2, 0, length.out = 25)
  bottom_shoulder <- seq(pi, 3*pi/2, length.out = 25)
  
  tibble(
    x = fatten_factor * c(
      radius * cos(top_shoulder) + radius,
      radius * cos(top_nose) - radius,
      radius * cos(bottom_nose) - radius,
      radius * cos(bottom_shoulder) + radius
    ) + center_x,
    y = c(
      radius * sin(top_shoulder) - radius + height / 2,
      radius * sin(top_nose) + radius,
      radius * sin(bottom_nose) - radius,
      radius * sin(bottom_shoulder) + radius - height / 2
    ) + center_y
  ) 
}

d <- tribble(
  ~category, ~negroes, ~france,
  "AGES,\nUNDER 10", 30.1, 17.5,
  "10-20", 26.1, 17.4,
  "20-30", 17.3, 16.3,
  "30-40", 10.6, 13.8,
  "40-50", 6.8, 12.3,
  "50-60", 4.6, 10.1,
  "60-70", 2.9, 7.6,
  "70 AND\nOVER", 1.6, 5) %>%
  mutate(category = factor(category, 
                           levels = rev(category))) %>%
  pivot_longer(-category, names_to = "race", values_to = "percent") %>%
  mutate(pretty_percent = paste0(percent, "%"),
         size = case_when(race == "negroes" & category == "50-60" ~ 6,
                          race == "negroes" & category == "60-70" ~ 5,
                          race == "negroes" & category == "70 AND\nOVER" ~ 4,
                          TRUE ~ 8))

braces <- tibble(index = 1:8) %>%
  mutate(axes = map(index, ~draw_brace(center_x = -0.6, center_y = .x,
                                       aspect_ratio = 1/4, height = 0.55,
                                       fatten_factor = 6))) %>%
  unnest(axes)

d %>%
  ggplot(aes(x = percent, y = category, fill = race, color = race,
             label = pretty_percent)) +
  geom_col(color = "black", position = position_dodge(), width = 0.5,
           linewidth = 0.1) +
  geom_text(aes(x = percent / 2),
            position = position_dodge(width = 0.5),
            family = "b52",
            size = d$size, size.unit = "pt",
            show.legend = FALSE) + 
  geom_path(data = braces,
            aes(x = x, y = y, group = index),
            linewidth = 0.1,
            inherit.aes = FALSE) +
  scale_fill_manual(
    name = NULL,
    breaks = c("negroes", "france"),
    values = c("#000000", "#ffd700"),
    labels = c("NEGROES.", "FRANCE.")) +
  scale_color_manual(
    name = NULL,
    breaks = c("negroes", "france"),
    values = c("#FFFFFF", "#000000"),
    labels = c("NEGROES.", "FRANCE.")) +
  coord_cartesian(expand = FALSE, clip = "off") +
  labs(title = "AGE DISTRIBUTION OF GEORGIA NEGROES\nCOMPARED WITH FRANCE.") +
  theme(
    legend.position = "top",
    axis.ticks = element_blank(),
    axis.title = element_blank(),
    axis.text.x = element_blank(),
    axis.text.y = element_text(family = "vasarely", size = 6, hjust = 0.5,
                               margin = margin(r = -3)),
    panel.grid = element_blank(),
    panel.background = element_blank(),
    plot.title = element_text(hjust = 0.5, family = "b52", size = 12,
                              margin = margin(t = 15)),
    legend.text = element_text(family = "vasarely", size = 6),
    legend.key.height = unit(4, "pt"),
    legend.key.spacing.x = unit(40, "pt"),
    legend.box.spacing = unit(20, "pt"),
    plot.margin = margin(b = 70, t = 5, l = 60, r = 120)
  )

ggsave("plate_09.png", width = 5, height = 6.31, unit = "in")
```