100% Stacked Area Chart — ggplot2

A stacked area chart normalized to 100%, where each area represents the percentage contribution of a category to the total. The combined height always equals 100%, showing proportional changes over time.

100% Stacked Area Chart rendered with ggplot2

Renders

R source (ggplot2)

#' anyplot.ai
#' area-stacked-percent: 100% Stacked Area Chart
#' Library: ggplot2 3.5.1 | R 4.4.1
#' Quality: 95/100 | Created: 2026-09-02

library(ggplot2)
library(dplyr)
library(ragg)

set.seed(42)

# --- Theme tokens -----------------------------------------------------------
THEME       <- Sys.getenv("ANYPLOT_THEME", "light")
PAGE_BG     <- if (THEME == "light") "#FAF8F1" else "#1A1A17"
INK         <- if (THEME == "light") "#1A1A17" else "#F0EFE8"
INK_SOFT    <- if (THEME == "light") "#4A4A44" else "#B8B7B0"
IMPRINT_PALETTE <- c("#009E73", "#C475FD", "#4467A3", "#BD8233", "#AE3030")

# --- Data ---------------------------------------------------------------
# Cloud infrastructure market share by vendor, quarterly, 2020-2024
quarters <- seq(as.Date("2020-01-01"), as.Date("2024-10-01"), by = "quarter")
vendors <- c("Vendor A", "Vendor B", "Vendor C", "Vendor D", "Vendor E")
n_periods <- length(quarters)

trend <- rbind(
  cumsum(rnorm(n_periods, mean = 1.4, sd = 1.0)) + 30,
  cumsum(rnorm(n_periods, mean = 0.6, sd = 1.0)) + 22,
  cumsum(rnorm(n_periods, mean = -0.2, sd = 1.0)) + 20,
  cumsum(rnorm(n_periods, mean = -0.9, sd = 1.0)) + 16,
  cumsum(rnorm(n_periods, mean = -1.1, sd = 1.0)) + 12
)
trend[trend < 2] <- 2

df <- expand.grid(quarter = quarters, vendor = vendors) %>%
  arrange(vendor, quarter) %>%
  mutate(revenue = as.vector(t(trend)))

df$vendor <- factor(df$vendor, levels = vendors)

# --- Direct end-of-line labels (replaces side legend) ------------------------
# Stacking order is reverse-factor (bottom-to-top: E, D, C, B, A), so cumulative
# shares must be computed in that same order to land labels on the right band.
last_quarter <- max(df$quarter)
label_df <- df %>%
  filter(quarter == last_quarter) %>%
  mutate(pct = revenue / sum(revenue)) %>%
  arrange(desc(vendor)) %>%
  mutate(ymax = cumsum(pct), ymin = ymax - pct, ymid = (ymin + ymax) / 2) %>%
  arrange(ymid)

# Nudge overlapping labels apart (thin bands, e.g. Vendor D/E, sit within a
# text-height of each other) so labels never collide, and keep the bottom
# label clear of the 0% axis line.
min_gap <- 0.06
label_df$ylabel <- label_df$ymid
label_df$ylabel[1] <- max(label_df$ylabel[1], 0.04)
for (i in 2:nrow(label_df)) {
  if (label_df$ylabel[i] - label_df$ylabel[i - 1] < min_gap) {
    label_df$ylabel[i] <- label_df$ylabel[i - 1] + min_gap
  }
}
label_df$label_x <- last_quarter + 60

# --- Plot -----------------------------------------------------------------
title_text <- "Cloud Market Share · area-stacked-percent · r · ggplot2 · anyplot.ai"
title_fontsize <- round(12 * min(1.0, 67 / nchar(title_text)))

p <- ggplot(df, aes(x = quarter, y = revenue, fill = vendor)) +
  geom_area(position = "fill", color = PAGE_BG, linewidth = 0.3) +
  scale_fill_manual(values = IMPRINT_PALETTE) +
  geom_segment(
    data = label_df,
    aes(x = last_quarter, xend = label_x, y = ymid, yend = ylabel, color = vendor),
    inherit.aes = FALSE, linewidth = 0.35
  ) +
  geom_text(
    data = label_df,
    aes(x = label_x, y = ylabel, label = vendor, color = vendor),
    inherit.aes = FALSE, hjust = 0, size = 3, fontface = "bold"
  ) +
  scale_color_manual(values = IMPRINT_PALETTE, guide = "none") +
  scale_y_continuous(labels = scales::percent_format(), expand = c(0, 0)) +
  scale_x_date(
    expand = expansion(mult = c(0, 0.02), add = c(0, 250)),
    date_labels = "%Y", date_breaks = "1 year"
  ) +
  coord_cartesian(clip = "off") +
  labs(
    title = title_text,
    x = "Quarter",
    y = "Market Share (%)"
  ) +
  theme_minimal(base_size = 8) +
  theme(
    legend.position     = "none",
    plot.background     = element_rect(fill = PAGE_BG, color = PAGE_BG),
    panel.background    = element_rect(fill = PAGE_BG, color = NA),
    panel.grid.major.y  = element_line(color = INK, linewidth = 0.2),
    panel.grid.minor    = element_blank(),
    panel.grid.major.x  = element_blank(),
    axis.title          = element_text(color = INK, size = 10),
    axis.text           = element_text(color = INK_SOFT, size = 8),
    axis.ticks          = element_blank(),
    plot.title          = element_text(color = INK, size = title_fontsize),
    plot.margin         = margin(t = 5, r = 34, b = 5, l = 5)
  )

# --- Save -------------------------------------------------------------------
ggsave(
  filename = sprintf("plot-%s.png", THEME),
  plot     = p,
  device   = ragg::agg_png,
  width    = 8,
  height   = 4.5,
  units    = "in",
  dpi      = 400
)

Retrieve this implementation

Runnable source as JSON, for any HTTP client: https://api.anyplot.ai/specs/area-stacked-percent/ggplot2/code. Any spec id and library id listed in llms-full.txt fit the same URL shape; every URL below is complete and callable.

{
  "spec_id": "area-stacked-percent",
  "language": "r",
  "library": "ggplot2",
  "page": "https://anyplot.ai/area-stacked-percent/r/ggplot2",
  "hub": "https://anyplot.ai/area-stacked-percent",
  "code_json": "https://api.anyplot.ai/specs/area-stacked-percent/ggplot2/code",
  "spec_json": "https://api.anyplot.ai/specs/area-stacked-percent",
  "render_light_png": "https://storage.googleapis.com/anyplot-images/plots/area-stacked-percent/r/ggplot2/plot-light.png",
  "render_dark_png": "https://storage.googleapis.com/anyplot-images/plots/area-stacked-percent/r/ggplot2/plot-dark.png",
  "quality_score": 95.0,
  "license": "MIT",
  "guide": "https://anyplot.ai/llms.txt"
}

Part of 100% Stacked Area Chart on anyplot.ai.

Other implementations