Pareto Chart with Cumulative Line — Altair

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 Altair

Python source (Altair)

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

import os

import altair as alt
import pandas as pd
from PIL import Image


# Theme tokens — Imprint palette, theme-adaptive chrome
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"

VITAL_COLOR = "#009E73"  # Imprint position 1 — vital few bars
LINE_COLOR = "#BD8233"  # Imprint position 4 — cumulative % line

# Data — manufacturing defect analysis
defects = pd.DataFrame(
    {
        "category": [
            "Scratches",
            "Dents",
            "Misalignment",
            "Cracks",
            "Discoloration",
            "Burrs",
            "Warping",
            "Contamination",
            "Missing Parts",
            "Wrong Dimensions",
        ],
        "count": [187, 128, 95, 72, 54, 38, 27, 19, 12, 8],
    }
)

defects = defects.sort_values("count", ascending=False).reset_index(drop=True)
total = defects["count"].sum()
defects["cumulative_pct"] = defects["count"].cumsum() / total * 100
defects["vital_few"] = defects["cumulative_pct"].shift(1, fill_value=0) < 80
sort_order = defects["category"].tolist()

threshold_df = pd.DataFrame({"pct": [80]})

# Title with length-scaled fontsize (floor 11, default 16 at 67 chars)
title_text = "Manufacturing Defect Analysis · bar-pareto · python · altair · anyplot.ai"
n = len(title_text)
title_fs = max(11, round(16 * 67 / n)) if n > 67 else 16

# Bars: vital few → Imprint green, trivial many → theme-adaptive muted
bars = (
    alt.Chart(defects)
    .mark_bar(cornerRadiusTopLeft=4, cornerRadiusTopRight=4)
    .encode(
        x=alt.X(
            "category:N",
            title="Defect Type",
            sort=sort_order,
            axis=alt.Axis(labelAngle=-45, labelFontSize=10, titleFontSize=12),
        ),
        y=alt.Y("count:Q", title="Frequency", axis=alt.Axis(labelFontSize=10, titleFontSize=12)),
        color=alt.condition(alt.datum.vital_few, alt.value(VITAL_COLOR), alt.value(INK_MUTED)),
        tooltip=[
            alt.Tooltip("category:N", title="Defect"),
            alt.Tooltip("count:Q", title="Count"),
            alt.Tooltip("cumulative_pct:Q", title="Cumulative %", format=".1f"),
        ],
    )
)

# Cumulative % line on secondary y-axis (Imprint ochre)
line = (
    alt.Chart(defects)
    .mark_line(
        color=LINE_COLOR,
        strokeWidth=3,
        point=alt.OverlayMarkDef(color=LINE_COLOR, size=80, filled=True, stroke=PAGE_BG, strokeWidth=1.5),
    )
    .encode(
        x=alt.X("category:N", sort=sort_order),
        y=alt.Y(
            "cumulative_pct:Q",
            title="Cumulative Percentage (%)",
            scale=alt.Scale(domain=[0, 105]),
            axis=alt.Axis(
                labelFontSize=10, titleFontSize=12, titleColor=LINE_COLOR, labelColor=LINE_COLOR, format=".0f"
            ),
        ),
        tooltip=[
            alt.Tooltip("category:N", title="Defect"),
            alt.Tooltip("cumulative_pct:Q", title="Cumulative %", format=".1f"),
        ],
    )
)

# 80% reference line
rule = (
    alt.Chart(threshold_df)
    .mark_rule(strokeDash=[8, 6], strokeWidth=1.5, color=INK_MUTED)
    .encode(y=alt.Y("pct:Q", scale=alt.Scale(domain=[0, 105])))
)

# 80% label
rule_label = (
    alt.Chart(pd.DataFrame({"pct": [80], "label": ["80%"]}))
    .mark_text(align="left", dx=5, dy=-8, fontSize=10, fontWeight="bold", color=INK_MUTED)
    .encode(x=alt.value(10), y=alt.Y("pct:Q", scale=alt.Scale(domain=[0, 105])), text="label:N")
)

# Compose: bars + cumulative line + reference line, independent y-scales
chart = (
    alt.layer(bars, line + rule + rule_label)
    .resolve_scale(y="independent")
    .properties(
        width=620,
        height=308,
        background=PAGE_BG,
        title=alt.Title(
            text=title_text,
            subtitle="Vital few categories (green) account for 80% of all defect occurrences",
            fontSize=title_fs,
            subtitleFontSize=10,
            subtitleColor=INK_SOFT,
            color=INK,
        ),
    )
    .configure_view(fill=PAGE_BG, strokeWidth=0)
    .configure_axis(domainColor=INK_SOFT, tickColor=INK_SOFT, grid=False, labelColor=INK_SOFT, titleColor=INK)
    .configure_axisY(grid=True, gridDash=[4, 4], gridColor=INK, gridOpacity=0.15)
    .configure_legend(fillColor=ELEVATED_BG, strokeColor=INK_SOFT, labelColor=INK_SOFT, titleColor=INK)
)

# Save and pad to exact 3200 × 1800
TW, TH = 3200, 1800
chart.save(f"plot-{THEME}.png", scale_factor=4.0)
chart.save(f"plot-{THEME}.html")

_img = Image.open(f"plot-{THEME}.png").convert("RGB")
_w, _h = _img.size
if _w > TW or _h > TH:
    raise SystemExit(
        f"altair vl-convert produced {_w}×{_h}, exceeds target {TW}×{TH}. "
        f"Shrink chart .properties(width=, height=) values and re-render."
    )
if _w < TW or _h < TH:
    _canvas = Image.new("RGB", (TW, TH), PAGE_BG)
    _canvas.paste(_img, ((TW - _w) // 2, (TH - _h) // 2))
    _canvas.save(f"plot-{THEME}.png")

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

Other implementations