Pareto Chart with Cumulative Line — Bokeh

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 Bokeh

Python source (Bokeh)

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

import os
import sys


# This file is named 'bokeh.py' — same as the package it imports.
# Remove the script's own directory from sys.path so 'from bokeh.io import ...'
# resolves to the installed bokeh package, not this file.
_sd = os.path.dirname(os.path.abspath(__file__))
sys.path = [p for p in sys.path if not p or os.path.abspath(p) != _sd]
del _sd

import time
from pathlib import Path

import numpy as np
from bokeh.io import output_file, save
from bokeh.models import ColumnDataSource, HoverTool, Label, LinearAxis, PrintfTickFormatter, Range1d, Span
from bokeh.plotting import figure
from bokeh.resources import INLINE
from selenium import webdriver
from selenium.webdriver.chrome.options import Options


# 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 = "#4467A3"  # Imprint position 3 — cumulative line
AMBER = "#DDCC77"  # semantic anchor — warning/threshold for 80% reference line

# Data — manufacturing defect types sorted descending by frequency
categories = [
    "Scratches",
    "Dents",
    "Misalignment",
    "Discoloration",
    "Cracks",
    "Burrs",
    "Warping",
    "Contamination",
    "Chipping",
    "Porosity",
]
counts = np.array([187, 143, 98, 72, 54, 38, 27, 19, 12, 7])
cumulative_pct = np.cumsum(counts) / counts.sum() * 100

# Vital few = bars up through the first one to push cumulative past 80%
vital_mask = np.zeros(len(counts), dtype=bool)
for i, pct in enumerate(cumulative_pct):
    vital_mask[i] = True
    if pct >= 80:
        break

bar_colors = [VITAL_COLOR if v else INK_MUTED for v in vital_mask]

source = ColumnDataSource(
    data={
        "categories": categories,
        "counts": counts.tolist(),
        "cumulative_pct": cumulative_pct.tolist(),
        "colors": bar_colors,
        "pct_label": [f"{p:.0f}%" for p in cumulative_pct],
    }
)

# Figure — 3200×1800 landscape; extra right border for secondary axis
p = figure(
    x_range=categories,
    width=3200,
    height=1800,
    title="bar-pareto · python · bokeh · anyplot.ai",
    x_axis_label="Defect Type",
    y_axis_label="Defect Count",
    toolbar_location=None,
    min_border_bottom=160,
    min_border_left=180,
    min_border_top=110,
    min_border_right=220,
)

# Bars — vital few in brand green, trivial many in muted neutral
p.vbar(
    x="categories",
    top="counts",
    source=source,
    width=0.72,
    color="colors",
    alpha=0.9,
    line_color=PAGE_BG,
    line_width=2,
    legend_label="Defect Count",
)

# Secondary y-axis for cumulative percentage (0–100%)
p.extra_y_ranges = {"pct": Range1d(start=0, end=105)}
pct_axis = LinearAxis(
    y_range_name="pct",
    axis_label="Cumulative %",
    axis_label_text_font_size="42pt",
    axis_label_text_color=INK,
    major_label_text_font_size="34pt",
    major_label_text_color=INK_SOFT,
    axis_line_color=INK_SOFT,
    axis_line_width=2,
    major_tick_line_color=None,
    minor_tick_line_color=None,
)
pct_axis.formatter = PrintfTickFormatter(format="%d%%")
p.add_layout(pct_axis, "right")

# Cumulative line
p.line(
    x="categories",
    y="cumulative_pct",
    source=source,
    y_range_name="pct",
    line_width=5,
    line_color=LINE_COLOR,
    line_join="round",
    legend_label="Cumulative %",
)

# Markers on cumulative line
p.scatter(
    x="categories",
    y="cumulative_pct",
    source=source,
    y_range_name="pct",
    size=18,
    color=LINE_COLOR,
    line_color=PAGE_BG,
    line_width=3,
)

# Percentage labels — use text glyph with categorical x for correct anchoring
# (Label.x must be numeric; p.text() accepts string categories via ColumnDataSource)
vital_indices = [i for i in range(len(categories)) if vital_mask[i]]
label_source = ColumnDataSource(
    data={
        "lx": [categories[i] for i in vital_indices],
        "ly": [float(cumulative_pct[i]) + 3 for i in vital_indices],  # 3 pct-pts above marker
        "lt": [f"{cumulative_pct[i]:.0f}%" for i in vital_indices],
    }
)
p.text(
    x="lx",
    y="ly",
    text="lt",
    source=label_source,
    y_range_name="pct",
    text_font_size="24pt",
    text_color=LINE_COLOR,
    text_font_style="bold",
    text_align="center",
    text_baseline="bottom",
)

# 80% reference line
p.add_layout(
    Span(
        location=80,
        dimension="width",
        line_color=AMBER,
        line_dash="dashed",
        line_width=3,
        line_alpha=0.9,
        y_range_name="pct",
    )
)

# 80% threshold label
p.add_layout(
    Label(
        x=9,
        y=80,
        text="80% threshold",
        text_font_size="26pt",
        text_color=AMBER,
        text_font_style="bold",
        text_align="right",
        x_offset=-10,
        y_offset=14,
        y_range_name="pct",
    )
)

# HoverTool — signature Bokeh interactive feature
p.add_tools(
    HoverTool(tooltips=[("Defect", "@categories"), ("Count", "@counts"), ("Cumulative", "@pct_label")], mode="vline")
)

# Title
p.title.text_font_size = "50pt"
p.title.align = "center"
p.title.text_color = INK

# Axis labels
p.xaxis.axis_label_text_font_size = "42pt"
p.yaxis.axis_label_text_font_size = "42pt"
p.xaxis.axis_label_text_color = INK
p.yaxis.axis_label_text_color = INK

# Tick labels
p.xaxis.major_label_text_font_size = "34pt"
p.yaxis.major_label_text_font_size = "34pt"
p.xaxis.major_label_text_color = INK_SOFT
p.yaxis.major_label_text_color = INK_SOFT
p.xaxis.major_label_orientation = 0.5

# Axis lines
p.xaxis.axis_line_color = INK_SOFT
p.yaxis.axis_line_color = INK_SOFT
p.xaxis.axis_line_width = 2
p.yaxis.axis_line_width = 2

# Remove tick marks
p.xaxis.major_tick_line_color = None
p.yaxis.major_tick_line_color = None
p.xaxis.minor_tick_line_color = None
p.yaxis.minor_tick_line_color = None

# Grid — horizontal only, very subtle
p.xgrid.grid_line_color = None
p.ygrid.grid_line_color = INK
p.ygrid.grid_line_alpha = 0.12

# Y range with headroom for legend in top-left
p.y_range.start = 0
p.y_range.end = max(counts) * 1.25

# Background and frame
p.outline_line_color = None
p.background_fill_color = PAGE_BG
p.border_fill_color = PAGE_BG

# Legend
if p.legend:
    legend = p.legend[0]
    legend.location = "top_left"
    legend.label_text_font_size = "34pt"
    legend.label_text_color = INK_SOFT
    legend.background_fill_color = ELEVATED_BG
    legend.border_line_color = INK_SOFT
    legend.padding = 15
    legend.spacing = 8

# Save HTML artifact (interactive) — inline resources so Selenium can render via file://
output_file(f"plot-{THEME}.html")
save(p, resources=INLINE)

# Screenshot with Selenium headless Chrome
W, H = 3200, 1800
opts = Options()
for arg in (
    "--headless=new",
    "--no-sandbox",
    "--disable-dev-shm-usage",
    "--disable-gpu",
    f"--window-size={W},{H}",
    "--hide-scrollbars",
):
    opts.add_argument(arg)
driver = webdriver.Chrome(options=opts)
# Headless Chrome's inner viewport is smaller than the outer window by ~143px
# (browser UI chrome). Compensate so the screenshot is exactly W×H pixels.
inner_h = driver.execute_script("return window.innerHeight")
if inner_h < H:
    driver.set_window_size(W, H + (H - inner_h))
driver.get(f"file://{Path(f'plot-{THEME}.html').resolve()}")
time.sleep(3)
driver.save_screenshot(f"plot-{THEME}.png")
driver.quit()

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

Other implementations