Court Review Viz

Making bar charts with data from the most popular Court Review articles.

This post walks through several approaches to creating bar charts that display article download statistics, including adding images, custom labels, and rounded bars. It uses data compiled from the Most Popular Court Review articles for the 60th aniversary issues.

Setup

Code
# Data wrangling
library(tidyverse)
library(janitor)
library(glue)
library(forcats)

# ggplot extensions
library(ggimage)
library(scales)
library(emwthemes)
library(cowplot)
library(ggtext)
library(prismatic)

# Fonts
library(sysfonts)
library(showtext)
library(ragg)

# Load ggchicklet code
source("ggchicklet.R")

# Load fonts
sysfonts::font_add_google("Sora", "Sora")
showtext::showtext_auto()

# Define aspect ratio (golden ratio)
asp_ratio <- 1.618 

# Custom ggplot theme
theme_custom <- function() { 
  theme_minimal(base_size = 16, base_family = "Sora") %+replace% 
    theme(
      panel.grid.minor = element_blank(),
      plot.background = element_rect(fill = "floralwhite", color = "floralwhite")
    )
}
Tip

Good blog post about how to select fonts for data viz projects.

Load and Prepare Data

Code
raw_data <- read_csv("data/cr_downloads.csv")

data <- raw_data |> 
  janitor::clean_names() |> 
  arrange(desc(downloads_new)) |> 
  mutate(
    rank_num = row_number(),
    rank = as.character(rank_num),
    downloads_comma = scales::comma(downloads_new),
    downloads = number_format(scale = 1, accuracy = 0.001)(round(downloads_new / 1000, 3)),
    downloads = as.numeric(downloads),
    downloads_text = if_else(
      rank_num == 1,
      paste(round(downloads, digits = 2), "thousand downloads"),
      as.character(round(downloads, digits = 2))
    ),
    lab_text = glue::glue("{rank}. {title} ({year})"),
    title_text = glue::glue("{title} ({year})"),
    image = glue::glue("images/CR{volume}-{issue}Cover.png"),
    img = glue::glue("https://emmarshall.github.io/runza/img/cr/CR{volume}-{issue}Cover.png")
  ) |>
  as_tibble()

glimpse(data)
Rows: 15
Columns: 28
$ title           <chr> "A Method for Analyzing the Accuracy of Eyewitness Tes…
$ volume          <dbl> 48, 48, 47, 44, 36, 49, 48, 45, 38, 47, 47, 50, 42, 45…
$ issue           <chr> "1_2", "1_2", "1_2", "1_2", "3", "2", "3", "1_2", "4",…
$ first_page      <dbl> 22, 36, 4, 26, 24, 114, 86, 4, 36, 8, 20, 136, 5, 112,…
$ last_page       <dbl> 34, 43, 7, 31, 31, 118, 95, 11, 40, 18, 31, 148, 6, 11…
$ year            <dbl> 2012, 2012, 2011, 2007, 1999, 2013, 2012, 2009, 2002, …
$ link            <chr> "https://digitalcommons.unl.edu/ajacourtreview/382/", …
$ downloads_new   <dbl> 13463, 9430, 8526, 6418, 6189, 5005, 4798, 3854, 2445,…
$ downloads_old   <dbl> 13450, 9387, 8371, 6089, 6104, 4524, 3843, 3764, 2441,…
$ author1_first   <chr> "Richard A.", "Fiona", "Steven M.", "Tom", "David B.",…
$ author1_last    <chr> "Wise", "Gabbert", "Smith", "Tyler", "Rottman", "Peer"…
$ author2_first   <chr> "Martin A.", "Daniel B", "Veronica", NA, "Alan", "Eyal…
$ author2_last    <chr> "Safer", "Wright", "Stinson", NA, "Tomkins", "Gamliel"…
$ author3_first   <chr> NA, "Amina", "Marc W.", NA, NA, NA, "Dorian M.", NA, N…
$ author3_last    <chr> NA, "Memon", "Patry", NA, NA, NA, "Wilderman", NA, NA,…
$ author4_first   <chr> NA, "Elin M.", NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
$ author4_last    <chr> NA, "Skagerberg", NA, NA, NA, NA, NA, NA, NA, NA, NA, …
$ author5_first   <chr> NA, "Kat", NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
$ author5_last    <chr> NA, "Jamieson", NA, NA, NA, NA, NA, NA, NA, NA, NA, NA…
$ image           <glue> "images/CR48-1_2Cover.png", "images/CR48-1_2Cover.png…
$ rank_num        <int> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15
$ rank            <chr> "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11…
$ downloads_comma <chr> "13,463", "9,430", "8,526", "6,418", "6,189", "5,005",…
$ downloads       <dbl> 13.463, 9.430, 8.526, 6.418, 6.189, 5.005, 4.798, 3.85…
$ downloads_text  <chr> "13.46 thousand downloads", "9.43", "8.53", "6.42", "6…
$ lab_text        <glue> "1. A Method for Analyzing the Accuracy of Eyewitness …
$ title_text      <glue> "A Method for Analyzing the Accuracy of Eyewitness Te…
$ img             <glue> "https://emmarshall.github.io/runza/img/cr/CR48-1_2Co…

Option 1: Bar Chart with Labels Above

This approach places the article title above each bar and the download count at the end of the bar.

Code
label_above <- data |> 
  mutate(lab_text = str_wrap(lab_text, width = 160)) |>
  ggplot(aes(x = downloads, y = forcats::fct_reorder(lab_text, downloads))) +
  geom_col(fill = "#c2545b", width = 0.2) +
  geom_text(
    aes(x = 0, y = fct_reorder(lab_text, downloads), label = lab_text),
    color = "#202C39", 
    lineheight = 0.3, 
    hjust = 0, 
    position = position_nudge(y = 0.49),
    fontface = "bold", 
    family = "Sora", 
    size = 9
  ) +
  geom_text(
    aes(x = downloads, y = fct_reorder(lab_text, downloads), label = downloads_text),
    color = "#202C39", 
    hjust = 1, 
    position = position_nudge(x = -0.02, y = 0.22),
    fontface = "bold", 
    family = "Sora", 
    size = 8
  ) +
  theme_minimal() +
  theme_custom() +
  theme(
    text = element_text(family = "Sora", face = "bold", size = 18),
    axis.text = element_blank(),
    axis.text.x = element_blank(),
    axis.ticks = element_blank(),
    panel.grid = element_blank()
  ) +
  labs(
    y = "",
    x = "Number of downloads in thousands"
  ) +
  scale_x_continuous(
    limits = c(0, 13.7), 
    expand = c(0, 0),
    breaks = scales::breaks_pretty(n = 15)
  )

ggsave(
  "plots/label_above.png", 
  label_above,
  device = ragg::agg_png, 
  dpi = 300,
  width = 16.5 * asp_ratio, 
  height = 16.5, 
  units = "cm"
)

Option 2: Bar Chart with Cover Images

Adding journal cover images makes the chart more visually engaging and helps readers identify articles.

Code
link_to_img <- function(x, width = 50) {
  glue::glue("<img src='{x}' width='{width}'/>")
}

logo_label_above <- data |> 
  mutate(
    lab_text = str_wrap(lab_text, width = 160),
    color = case_when(
      rank == "1" ~ "#c2545b",
      rank == "2" ~ "#c2545b",
      TRUE ~ "grey70"
    ), 
    logo_label = link_to_img(img)
  ) |> 
  ggplot(aes(x = downloads, y = forcats::fct_reorder(lab_text, downloads), fill = color)) +
  geom_col(width = 0.2) +
  ggtext::geom_richtext(
    aes(y = -0.1, label = logo_label, hjust = 0),
    label.size = 0, 
    fill = NA
  ) +
  geom_text(
    aes(x = downloads, y = fct_reorder(lab_text, downloads), label = downloads_text),
    color = "#202C39", 
    hjust = 1, 
    position = position_nudge(x = -0.02, y = 0.22),
    fontface = "bold", 
    family = "Sora", 
    size = 8
  ) +
  scale_fill_identity() +
  theme_minimal() +
  theme_custom() +
  theme(
    text = element_text(family = "Sora", face = "bold", size = 24),
    axis.text = element_blank(),
    axis.text.x = element_blank(),
    axis.ticks = element_blank(),
    panel.grid = element_blank()
  ) +
  labs(
    y = "",
    x = "Number of downloads in thousands"
  ) +
  scale_x_continuous(
    limits = c(0, 13.7), 
    expand = c(0, 0),
    breaks = scales::breaks_pretty(n = 15)
  )

ggsave(
  "plots/logo_label_above.png",
  logo_label_above,
  device = ragg::agg_png, 
  dpi = 300,
  height = 16, 
  width = 16 * asp_ratio, 
  units = "cm"
)

Creating Formatted Author Names

For some visualizations, we want to display author names instead of article titles. This code creates a formatted author list with proper punctuation.

Code
# Replace NA with empty string
data <- data |> 
  mutate(across(starts_with("author"), ~ifelse(is.na(.), "", .)))

# Create the author names column
data$author_names <- apply(data, 1, function(row) {
  author_list <- c()
  
  for (i in 1:5) {
    first <- row[paste0("author", i, "_first")]
    last <- row[paste0("author", i, "_last")]
    if (nchar(first) > 0 && nchar(last) > 0) {
      author_list <- c(author_list, paste(first, last))
    }
  }
  
  if (length(author_list) > 1) {
    paste(author_list, collapse = ", ")
  } else {
    author_list
  }
})

# Replace the last comma with "&" to create proper author list
data <- data |> 
  mutate(
    author_names = str_replace(author_names, ",([^,]*)$", " &\\1"),
    label_author = glue::glue("{rank}. {author_names} ({year})")
  )

glimpse(data)
Rows: 15
Columns: 30
$ title           <chr> "A Method for Analyzing the Accuracy of Eyewitness Tes…
$ volume          <dbl> 48, 48, 47, 44, 36, 49, 48, 45, 38, 47, 47, 50, 42, 45…
$ issue           <chr> "1_2", "1_2", "1_2", "1_2", "3", "2", "3", "1_2", "4",…
$ first_page      <dbl> 22, 36, 4, 26, 24, 114, 86, 4, 36, 8, 20, 136, 5, 112,…
$ last_page       <dbl> 34, 43, 7, 31, 31, 118, 95, 11, 40, 18, 31, 148, 6, 11…
$ year            <dbl> 2012, 2012, 2011, 2007, 1999, 2013, 2012, 2009, 2002, …
$ link            <chr> "https://digitalcommons.unl.edu/ajacourtreview/382/", …
$ downloads_new   <dbl> 13463, 9430, 8526, 6418, 6189, 5005, 4798, 3854, 2445,…
$ downloads_old   <dbl> 13450, 9387, 8371, 6089, 6104, 4524, 3843, 3764, 2441,…
$ author1_first   <chr> "Richard A.", "Fiona", "Steven M.", "Tom", "David B.",…
$ author1_last    <chr> "Wise", "Gabbert", "Smith", "Tyler", "Rottman", "Peer"…
$ author2_first   <chr> "Martin A.", "Daniel B", "Veronica", "", "Alan", "Eyal…
$ author2_last    <chr> "Safer", "Wright", "Stinson", "", "Tomkins", "Gamliel"…
$ author3_first   <chr> "", "Amina", "Marc W.", "", "", "", "Dorian M.", "", "…
$ author3_last    <chr> "", "Memon", "Patry", "", "", "", "Wilderman", "", "",…
$ author4_first   <chr> "", "Elin M.", "", "", "", "", "", "", "", "", "", "",…
$ author4_last    <chr> "", "Skagerberg", "", "", "", "", "", "", "", "", "", …
$ author5_first   <chr> "", "Kat", "", "", "", "", "", "", "", "", "", "", "",…
$ author5_last    <chr> "", "Jamieson", "", "", "", "", "", "", "", "", "", ""…
$ image           <glue> "images/CR48-1_2Cover.png", "images/CR48-1_2Cover.png…
$ rank_num        <int> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15
$ rank            <chr> "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11…
$ downloads_comma <chr> "13,463", "9,430", "8,526", "6,418", "6,189", "5,005",…
$ downloads       <dbl> 13.463, 9.430, 8.526, 6.418, 6.189, 5.005, 4.798, 3.85…
$ downloads_text  <chr> "13.46 thousand downloads", "9.43", "8.53", "6.42", "6…
$ lab_text        <glue> "1. A Method for Analyzing the Accuracy of Eyewitness …
$ title_text      <glue> "A Method for Analyzing the Accuracy of Eyewitness Te…
$ img             <glue> "https://emmarshall.github.io/runza/img/cr/CR48-1_2Co…
$ author_names    <chr> "Richard A. Wise & Martin A. Safer", "Fiona Gabbert, …
$ label_author    <glue> "1. Richard A. Wise & Martin A. Safer (2012)", "2. Fio…

Option 3: Bar Chart with Author Names

Code
label_author <- data |> 
  mutate(label_author = str_wrap(lab_text, width = 115)) |>
  ggplot(aes(x = downloads, y = forcats::fct_reorder(lab_text, downloads))) +
  geom_col(fill = "#c2545b", width = 0.2) +
  geom_text(
    aes(x = 0, y = fct_reorder(label_author, downloads), label = label_author),
    color = "#202C39", 
    lineheight = 0.4, 
    hjust = 0, 
    position = position_nudge(y = 0.48),
    fontface = "bold", 
    family = "Sora", 
    size = 12
  ) +
  geom_text(
    aes(x = downloads, y = fct_reorder(label_author, downloads), label = downloads),
    color = "#202C39", 
    hjust = 1, 
    position = position_nudge(x = -0.02, y = 0.22),
    fontface = "bold", 
    family = "Sora", 
    size = 9
  ) +
  theme_minimal() +
  theme_custom() +
  theme(
    text = element_text(family = "Sora", face = "bold", size = 16),
    axis.text = element_blank(),
    axis.text.x = element_blank(),
    axis.ticks = element_blank(),
    panel.grid = element_blank()
  ) +
  labs(
    y = "",
    x = "Number of downloads in thousands"
  ) +
  scale_y_discrete(labels = scales::label_wrap(45)) +
  scale_x_continuous(
    limits = c(0, 13.7), 
    expand = c(0, 0),
    breaks = scales::breaks_pretty(n = 15)
  )

ggsave(
  "plots/label_author.png", 
  label_author,
  device = ragg::agg_png, 
  dpi = 300,
  width = 16 * asp_ratio, 
  height = 16, 
  units = "cm"
)

Option 4: Scatter Plot with Cover Images

Using geom_image() to place cover images directly on a plot creates a unique visualization.

Code
library(here)
base_dir <- here::here()

p <- ggplot(data, aes(x = as.character(year), y = downloads)) + 
  geom_image(aes(image = image), size = 0.12) +
  scale_y_continuous(
    limits = c(0, 13.7), 
    expand = c(0, 1.5),
    breaks = scales::breaks_pretty(n = 5)
  ) +
  scale_x_discrete(
    labels = function(x) format(as.Date(x, format = "%Y"), "%Y"), 
    expand = c(0, 1)
  ) +
  theme_custom() +
  theme(
    text = element_text(family = "Sora")
  ) +
  labs(
    x = "Year",
    y = "Downloads (thousands)"
  )

p

Option 5: Bar Chart with Embedded Images

Combining geom_col() with geom_image() places the cover image at the end of each bar.

Code
image_bar <- data |> 
  filter(rank_num <= 10) |> 
  ggplot(aes(x = fct_reorder(lab_text, downloads), y = downloads)) +
  geom_col(
    aes(
      fill = "#c2545b", 
      color = after_scale(clr_darken(fill, 0.3))
    ),
    width = 0.3, 
    alpha = 0.75
  ) + 
  scale_color_identity(aesthetics = c("fill")) +
  geom_image(
    aes(image = image), 
    size = 0.05, 
    by = "width", 
    asp = asp_ratio
  ) +
  theme_custom() +
  theme(
    text = element_text(family = "Sora"),
    axis.text.x = element_blank(), 
    panel.grid.major.x = element_blank(),
    plot.title = element_text(face = "bold", size = 36), 
    plot.title.position = "plot"
  ) + 
  scale_y_continuous(breaks = scales::pretty_breaks(n = 10)) +
  labs(
    y = "", 
    x = "Number of downloads", 
    title = "Court Review articles", 
    subtitle = paste0("Most downloaded articles. As of ", format.Date(Sys.Date(), "%b. %d, %Y")), 
    caption = "Source: UNLCommons/Emma Marshall"
  )

ggsave(
  "plots/image_bar.png", 
  image_bar,
  device = ragg::agg_png, 
  dpi = 300,
  height = 16, 
  width = 16 * asp_ratio, 
  units = "cm"
)

Option 6: Rounded Bars with ggchicklet

The ggchicklet package provides geom_chicklet() which creates bars with rounded ends for a softer, more modern look.

Code
link_to_img <- function(x, width = 30) {
  glue::glue("<img src='{x}' width='{width}'/>")
}

logo_bar <- data |> 
  mutate(
    color = case_when(
      rank == "1" ~ "#c2545b",
      rank == "2" ~ "#c2545b",
      TRUE ~ "grey70"
    ), 
    logo_label = link_to_img(img),
    title_text = case_when(
      downloads >= 5 & downloads <= 7 ~ str_wrap(title_text, width = 65),
      downloads < 5 ~ str_wrap(title_text, width = 102),
      TRUE ~ title_text
    )
  ) |> 
  filter(rank_num <= 10) |> 
  ggplot(aes(x = fct_reorder(lab_text, downloads), y = downloads, fill = color)) +
  geom_chicklet() +
  ggtext::geom_richtext(
    aes(y = -0.7, label = logo_label, hjust = 0),
    label.size = 0, 
    fill = NA
  ) +
  geom_text(
    aes(
      y = ifelse(downloads < 5, downloads + 0.2, 0.15), 
      label = title_text, 
      color = downloads < 9, 
      hjust = 0
    ), 
    family = "Sora", 
    fontface = "bold", 
    size = 9, 
    lineheight = 0.28
  ) +
  scale_color_manual(values = c("white", "black"), guide = "none") +
  scale_fill_identity(guide = "none") +
  coord_flip(clip = "off") +
  theme_custom() + 
  theme(
    text = element_text(family = "Sora", face = "bold", size = 24),
    axis.text.x = element_text(family = "Sora", face = "bold", size = 24),
    axis.text.y = element_blank(),
    panel.grid.major.x = element_blank(),
    panel.grid.major.y = element_blank(),
    plot.title = element_text(face = "bold", size = 36, hjust = 0.5), 
    plot.subtitle = element_text(hjust = 0.5),
    plot.title.position = "plot",
    legend.position = "none"
  ) +
  labs(
    x = "", 
    y = "", 
    title = "Most Popular Court Review Articles", 
    subtitle = "Top downloaded articles from UNL commons.", 
    caption = "Data: UNL Commons | Plot: Emma Marshall"
  )

ggsave(
  "plots/logo_bar.png", 
  logo_bar,
  device = ragg::agg_png, 
  dpi = 300,
  height = 18, 
  width = 18 * asp_ratio, 
  units = "cm"
)

Summary

This post demonstrated several approaches to visualizing download statistics:

  1. Labels above bars — clean and readable for long titles
  2. Cover images — adds visual interest and helps identification
  3. Author names — useful when authorship is more important than title
  4. Scatter with images — shows distribution over time
  5. Bars with embedded images — combines bar length with visual identification
  6. Rounded bars (ggchicklet) — modern, polished appearance with cover thumbnails

Key techniques used:

  • ggtext::geom_richtext() — embed HTML/images in plots
  • ggimage::geom_image() — place images at data points
  • geom_chicklet() — rounded bar ends
  • prismatic::clr_darken() — programmatic color adjustment
  • position_nudge() — fine-tune label placement
Back to top