Pareto Chart with Cumulative Line — ggplot2

A Pareto chart combining descending-sorted bars (by frequency or count) with a cumulative percentage line overlay on a secondary y-axis. This visualization helps identify the most significant factors in a dataset by applying the Pareto principle (80/20 rule), making it one of the "7 Basic Tools of Quality" in Six Sigma and quality management. It reveals which categories contribute the most to an overall effect, enabling data-driven prioritization.

Pareto Chart with Cumulative Line rendered with ggplot2

R source (ggplot2)

#' anyplot.ai
#' bar-pareto: Pareto Chart with Cumulative Line
#' Library: ggplot2 3.5.1 | R 4.4.1
#' Quality: 91/100 | Created: 2026-06-20

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

# --- Theme tokens ---
THEME       <- Sys.getenv("ANYPLOT_THEME", "light")
PAGE_BG     <- if (THEME == "light") "#FAF8F1" else "#1A1A17"
ELEVATED_BG <- if (THEME == "light") "#FFFDF6" else "#242420"
INK         <- if (THEME == "light") "#1A1A17" else "#F0EFE8"
INK_SOFT    <- if (THEME == "light") "#4A4A44" else "#B8B7B0"
IMPRINT_PALETTE <- c(
    "#009E73", "#C475FD", "#4467A3", "#BD8233",
    "#AE3030", "#2ABCCD", "#954477", "#99B314"
)
ANYPLOT_AMBER <- "#DDCC77"

# --- Data ---
# Manufacturing defect data: two root causes dominate (Pareto principle)
defect_data <- data.frame(
    category = c(
        "Surface Scratches", "Misalignment", "Paint Defects",
        "Weld Flaws", "Assembly Gaps", "Contamination",
        "Thread Damage", "Other"
    ),
    count = c(500, 300, 80, 55, 30, 18, 10, 7)
)

# Sort descending and compute cumulative sums
defect_data <- defect_data |>
    arrange(desc(count)) |>
    mutate(
        category  = factor(category, levels = category),
        cum_count = cumsum(count)
    )

total_count     <- sum(defect_data$count)
threshold_count <- 0.8 * total_count

# --- Plot ---
plot_title <- "Manufacturing Defects · bar-pareto · r · ggplot2 · anyplot.ai"

p <- ggplot(defect_data, aes(x = category)) +

    # Bars (primary axis: raw counts, fill-mapped for legend)
    geom_col(
        aes(y = count, fill = "Defect count"),
        width = 0.7,
        alpha = 0.92
    ) +

    # 80% Pareto threshold reference line (color-mapped for legend)
    geom_hline(
        aes(yintercept = threshold_count, color = "80% threshold"),
        linewidth = 0.85,
        linetype  = "dashed"
    ) +

    # Cumulative percentage line (color-mapped for legend)
    geom_line(
        aes(y = cum_count, group = 1, color = "Cumulative %"),
        linewidth = 1.3
    ) +

    # Markers at center-top of each bar
    geom_point(
        aes(y = cum_count, color = "Cumulative %"),
        size  = 3.5,
        shape = 19
    ) +

    # Annotation: highlight where cumulative % crosses 80%
    annotate(
        "text",
        x     = 2.5,
        y     = threshold_count + total_count * 0.04,
        label = "Top 2 → 80% of defects",
        color = ANYPLOT_AMBER,
        size  = 2.6,
        hjust = 0
    ) +

    # Dual y-axis: primary = count, secondary = cumulative %
    scale_y_continuous(
        name     = "Defect count",
        limits   = c(0, total_count),
        expand   = expansion(mult = c(0, 0)),
        labels   = scales::comma,
        sec.axis = sec_axis(
            ~ . / total_count * 100,
            name   = "Cumulative percentage (%)",
            breaks = seq(0, 100, 20),
            labels = function(x) paste0(x, "%")
        )
    ) +

    scale_x_discrete(expand = expansion(add = c(0.6, 0.6))) +

    # Legend: fill scale for bars
    scale_fill_manual(
        name   = NULL,
        values = c("Defect count" = IMPRINT_PALETTE[1])
    ) +

    # Legend: color scale for line and reference
    scale_color_manual(
        name   = NULL,
        values = c(
            "Cumulative %"  = IMPRINT_PALETTE[3],
            "80% threshold" = ANYPLOT_AMBER
        ),
        guide = guide_legend(
            override.aes = list(
                linetype  = c("solid", "dashed"),
                shape     = c(16, NA),
                linewidth = c(1.3, 0.85)
            )
        )
    ) +

    labs(
        x     = "Defect category",
        title = plot_title
    ) +

    theme_minimal(base_size = 8) +
    theme(
        plot.background    = element_rect(fill = PAGE_BG, color = PAGE_BG),
        panel.background   = element_rect(fill = PAGE_BG, color = NA),
        # L-frame: bottom and left axis lines only (remove full box border)
        panel.border       = element_blank(),
        axis.line.x.bottom = element_line(color = INK_SOFT, linewidth = 0.4),
        axis.line.y.left   = element_line(color = INK_SOFT, linewidth = 0.4),
        panel.grid.major.y = element_line(
            color     = adjustcolor(INK, alpha.f = 0.12),
            linewidth = 0.35
        ),
        panel.grid.major.x = element_blank(),
        panel.grid.minor   = element_blank(),
        axis.title.x       = element_text(color = INK,                size = 10),
        axis.title.y.left  = element_text(color = INK,                size = 10),
        axis.title.y.right = element_text(color = IMPRINT_PALETTE[3], size = 10),
        axis.text.x        = element_text(color = INK_SOFT, size = 7.5, angle = 32, hjust = 1),
        axis.text.y.left   = element_text(color = INK_SOFT,           size = 8),
        # Use INK_SOFT for right-axis ticks in dark mode (better contrast vs. #1A1A17)
        axis.text.y.right  = element_text(
            color = if (THEME == "dark") INK_SOFT else IMPRINT_PALETTE[3],
            size  = 8
        ),
        axis.ticks         = element_line(color = INK_SOFT, linewidth = 0.3),
        plot.title         = element_text(color = INK, size = 12, face = "bold",
                                          margin = margin(b = 8)),
        plot.margin        = margin(t = 15, r = 15, b = 10, l = 10, unit = "pt"),
        legend.position    = "bottom",
        legend.background  = element_rect(fill = ELEVATED_BG, color = NA),
        legend.text        = element_text(color = INK_SOFT, size = 8),
        legend.key         = element_rect(fill = NA, color = NA),
        legend.key.size    = unit(1, "lines"),
        legend.box         = "horizontal",
        legend.margin      = margin(t = 2, b = 4, l = 4, r = 4)
    )

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

Part of Pareto Chart with Cumulative Line on anyplot.ai.

Other implementations