Nested Column Bar Chart

Author

Win Khaing

Overview

This code creates an example Nested Column Chart of the type developed by Brittany Rosenau.

Setup

library(ggplot2)
library(dplyr)
library(readr)

Create Sample Data

csv_data <- "Region,Segment,Revenue
West,Consumer,364
West,Corporate,232
West,Home Office,143
East,Consumer,357
East,Corporate,204
East,Home Office,131
Central,Consumer,254
Central,Corporate,158
Central,Home Office,91
South,Consumer,196
South,Corporate,122
South,Home Office,74"

df <- read_csv(csv_data, show_col_types = FALSE)
df
# A tibble: 12 × 3
   Region  Segment     Revenue
   <chr>   <chr>         <dbl>
 1 West    Consumer        364
 2 West    Corporate       232
 3 West    Home Office     143
 4 East    Consumer        357
 5 East    Corporate       204
 6 East    Home Office     131
 7 Central Consumer        254
 8 Central Corporate       158
 9 Central Home Office      91
10 South   Consumer        196
11 South   Corporate       122
12 South   Home Office      74

Data Preparation

# Ensure Region is a factor with the desired order
df$Region <- factor(df$Region, levels = c("West", "East", "Central", "South"))

# Ensure Segment is a factor with the desired order for legend/coloring
df$Segment <- factor(df$Segment, levels = c("Consumer", "Corporate", "Home Office"))

# Calculate total revenue per region
region_totals <- df %>%
    group_by(Region) %>%
    summarise(TotalRevenue = sum(Revenue), .groups = "drop")

region_totals
# A tibble: 4 × 2
  Region  TotalRevenue
  <fct>          <dbl>
1 West             739
2 East             692
3 Central          503
4 South            392

Define Custom Colors

segment_colors <- c(
    "Consumer" = "#2ECC71",    # Green
    "Corporate" = "#F39C12",   # Orange
    "Home Office" = "#3498DB"  # Blue
)

Create the Nested Column Chart

p <- ggplot() +
    # Layer 1: Grey background bars representing total revenue per region
    geom_col(
        data = region_totals,
        aes(x = Region, y = TotalRevenue),
        fill = "grey85",
        alpha = 0.8,
        width = 0.8
    ) +
    # Layer 2: Dodged bars for segment revenue
    geom_col(
        data = df,
        aes(x = Region, y = Revenue, fill = Segment),
        position = position_dodge(width = 0.8),
        width = 0.7
    ) +
    # Layer 3: Labels for individual segment bars
    geom_text(
        data = df,
        aes(x = Region, y = Revenue, label = paste0("$", Revenue, "K"), group = Segment),
        position = position_dodge(width = 0.8),
        vjust = -0.5,
        size = 2.75,
        family = "sans"
    ) +
    # Layer 4: Labels for total region revenue
    geom_text(
        data = region_totals,
        aes(x = Region, y = TotalRevenue, label = paste0("$", TotalRevenue, "K")),
        vjust = -2.0,
        size = 3.5,
        family = "sans"
    ) +
    # Layer 5: Labels for Region Names
    geom_text(
        data = region_totals,
        aes(x = Region, y = TotalRevenue, label = as.character(Region)),
        vjust = -3.5,
        size = 4,
        fontface = "bold",
        family = "sans"
    ) +
    # Apply custom color scale
    scale_fill_manual(values = segment_colors) +
    # Set plot title
    ggtitle("Total Sales by Region and Segment ($K)") +
    # Customize theme
    theme_void(base_size = 12, base_family = "sans") +
    theme(
        plot.title = element_text(
            hjust = 0,
            face = "bold",
            size = 16,
            margin = margin(b = 15)
        ),
        legend.position = "top",
        legend.justification = "left",
        legend.title = element_blank(),
        legend.text = element_text(size = 11),
        legend.key.size = unit(0.5, "cm"),
        legend.margin = margin(b = -10),
        axis.text = element_blank(),
        axis.ticks = element_blank(),
        axis.title = element_blank(),
        plot.margin = margin(20, 20, 20, 20)
    ) +
    # Adjust y-axis limits for label space
    scale_y_continuous(
        limits = c(0, max(region_totals$TotalRevenue) * 1.35),
        expand = expansion(mult = c(0, 0))
    )

print(p)