Pareto Chart with Cumulative Line — lets-plot

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 lets-plot

Python source (lets-plot)

""" anyplot.ai
bar-pareto: Pareto Chart with Cumulative Line
Library: letsplot 4.10.1 | Python 3.13.14
Quality: 91/100 | Updated: 2026-06-20
"""

import os

import numpy as np
import pandas as pd
from lets_plot import *
from lets_plot import ggsave


LetsPlot.setup_html()

THEME = os.getenv("ANYPLOT_THEME", "light")
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F"
GRID_COLOR = "#E0DFD8" if THEME == "light" else "#2A2A27"

# Imprint palette — canonical order
IMPRINT_PALETTE = ["#009E73", "#C475FD", "#4467A3", "#BD8233", "#AE3030", "#2ABCCD", "#954477", "#99B314"]
BRAND = IMPRINT_PALETTE[0]  # brand green — bars contributing to 80%
CUMLINE_COLOR = IMPRINT_PALETTE[1]  # lavender — cumulative percentage line
THRESHOLD_COLOR = "#DDCC77"  # amber — warning/caution semantic anchor

# Data — manufacturing defect types sorted by frequency (descending)
categories = ["Scratches", "Dents", "Misalignment", "Cracks", "Discoloration", "Burrs", "Warping", "Contamination"]
counts = [186, 145, 98, 72, 54, 38, 27, 16]

df = pd.DataFrame({"category": categories, "count": counts})

# Cumulative percentage
total = sum(counts)
cumulative_pct = np.cumsum(counts) / total * 100

# Scale cumulative percentages to share the primary y-axis
max_count = max(counts)
y_max = total
scale_factor = y_max / 100

cumulative_scaled = cumulative_pct * scale_factor
threshold_80_scaled = 80 * scale_factor

# Semantic bar colors: brand green for ≤80% threshold, muted for the tail
bar_colors = [BRAND if cumulative_pct[i] <= 80 else INK_MUTED for i in range(len(categories))]
df["bar_color"] = bar_colors

# Segments to draw cumulative line across discrete x-axis
seg_df = pd.DataFrame(
    {
        "x": categories[:-1],
        "xend": categories[1:],
        "y": cumulative_scaled[:-1].tolist(),
        "yend": cumulative_scaled[1:].tolist(),
    }
)

# Points for cumulative line markers
df_points = pd.DataFrame(
    {
        "category": categories,
        "cumulative_scaled": cumulative_scaled.tolist(),
        "cumulative_pct": [f"{p:.0f}%" for p in cumulative_pct],
    }
)

# Simulated secondary y-axis tick labels (right of last bar)
sec_ticks = [20, 40, 60, 80, 100]
sec_labels_df = pd.DataFrame(
    {
        "category": [categories[-1]] * len(sec_ticks),
        "y": [t * scale_factor for t in sec_ticks],
        "label": [f"{t}%" for t in sec_ticks],
    }
)

# Title with dynamic font size (16px baseline for ~67-char title)
title = "bar-pareto · python · letsplot · anyplot.ai"
n = len(title)
title_size = round(16 * 67 / n) if n > 67 else 16
title_size = max(title_size, 11)

# Plot
plot = (
    ggplot(df, aes(x="category", y="count"))
    + geom_bar(
        aes(fill="bar_color"),
        stat="identity",
        width=0.72,
        tooltips=layer_tooltips()
        .title("@category")
        .line("Count|@count")
        .format("count", "d"),
        show_legend=False,
    )
    + scale_fill_identity()
    # Cumulative percentage line (segments span discrete axis positions)
    + geom_segment(
        data=seg_df,
        mapping=aes(x="x", y="y", xend="xend", yend="yend"),
        color=CUMLINE_COLOR,
        size=2.0,
        inherit_aes=False,
    )
    # Cumulative line markers
    + geom_point(
        data=df_points,
        mapping=aes(x="category", y="cumulative_scaled"),
        color=CUMLINE_COLOR,
        fill=PAGE_BG,
        size=5,
        shape=21,
        stroke=2.0,
        inherit_aes=False,
        tooltips=layer_tooltips().line("Cumulative|@cumulative_pct"),
    )
    # 80% threshold reference line (amber warning anchor)
    + geom_hline(yintercept=threshold_80_scaled, color=THRESHOLD_COLOR, size=1.0, linetype="dashed")
    + geom_text(
        data=pd.DataFrame({"category": [categories[0]], "y": [threshold_80_scaled], "label": ["80%"]}),
        mapping=aes(x="category", y="y", label="label"),
        color=THRESHOLD_COLOR,
        size=9,
        hjust=0.0,
        vjust=-0.7,
        fontface="bold",
        inherit_aes=False,
    )
    # Simulated secondary y-axis labels (right of last category)
    + geom_text(
        data=sec_labels_df[sec_labels_df["label"] != "100%"],
        mapping=aes(x="category", y="y", label="label"),
        color=CUMLINE_COLOR,
        size=6,
        hjust=-1.6,
        fontface="bold",
        inherit_aes=False,
    )
    + geom_text(
        data=sec_labels_df[sec_labels_df["label"] == "100%"],
        mapping=aes(x="category", y="y", label="label"),
        color=CUMLINE_COLOR,
        size=6,
        hjust=-1.6,
        vjust=1.8,
        fontface="bold",
        inherit_aes=False,
    )
    # Cumulative percentage annotations on first 3 points
    + geom_text(
        data=df_points.iloc[:3],
        mapping=aes(x="category", y="cumulative_scaled", label="cumulative_pct"),
        color=CUMLINE_COLOR,
        size=9,
        vjust=-1.5,
        fontface="bold",
        inherit_aes=False,
    )
    + scale_x_discrete(limits=categories)
    + scale_y_continuous(limits=[0, y_max], expand=[0, 0, 0.05, 0])
    + labs(
        x="Defect Type", y="Frequency (Count)", title=title, caption="Line: cumulative %  ·  Dashed: 80% threshold"
    )
    + theme_minimal()
    + theme(
        axis_text_x=element_text(angle=45, hjust=1, size=16, color=INK_SOFT),
        axis_text_y=element_text(size=16, color=INK_SOFT),
        axis_title=element_text(size=20, color=INK),
        plot_title=element_text(size=title_size, hjust=0.5, face="bold", color=INK),
        plot_caption=element_text(size=14, color=INK_SOFT, hjust=0.5),
        panel_grid_major_x=element_blank(),
        panel_grid_minor=element_blank(),
        panel_grid_major_y=element_line(color=GRID_COLOR, size=0.3),
        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),
        panel_background=element_rect(fill=PAGE_BG),
        legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),
        axis_line=element_line(color=INK_SOFT),
        plot_margin=[20, 90, 10, 10],
    )
    + ggsize(800, 450)
)

# Save — scale=4 yields 3200×1800 px from the 800×450 base
ggsave(plot, f"plot-{THEME}.png", path=".", scale=4)
ggsave(plot, f"plot-{THEME}.html", path=".")

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

Other implementations