Statistical Process Control Chart (X-bar/R) — Seaborn

A Statistical Process Control (SPC) chart displaying sample means (X-bar) and ranges (R) plotted over time against control limits. The chart includes a center line representing the process mean, Upper Control Limit (UCL) and Lower Control Limit (LCL) at ±3 sigma, and optional warning limits at ±2 sigma. Out-of-control points are highlighted to signal process instability. This is a fundamental tool in manufacturing quality control and Six Sigma methodology for monitoring process stability.

Statistical Process Control Chart (X-bar/R) rendered with Seaborn

Python source (Seaborn)

""" anyplot.ai
spc-xbar-r: Statistical Process Control Chart (X-bar/R)
Library: seaborn 0.13.2 | Python 3.13.14
Quality: 87/100 | Updated: 2026-06-20
"""

import os

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns


# Theme tokens — Imprint palette + 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"

BRAND = "#009E73"  # Imprint pos 1 — primary data series
BLUE = "#4467A3"  # Imprint pos 3 — center line
OOC_COLOR = "#AE3030"  # Imprint semantic red — out-of-control / error
WARN_COLOR = "#DDCC77"  # Imprint semantic amber — warning / caution limits

# Seaborn theme with Imprint chrome tokens
sns.set_theme(
    style="ticks",
    rc={
        "figure.facecolor": PAGE_BG,
        "axes.facecolor": PAGE_BG,
        "axes.edgecolor": INK_SOFT,
        "axes.labelcolor": INK,
        "text.color": INK,
        "xtick.color": INK_SOFT,
        "ytick.color": INK_SOFT,
        "grid.color": INK,
        "grid.alpha": 0.12,
        "legend.facecolor": ELEVATED_BG,
        "legend.edgecolor": INK_SOFT,
    },
)

# Data — injection molding thickness measurements (target: 3.200 mm)
np.random.seed(7)

n_samples = 30
subgroup_size = 5
target = 3.200
process_std = 0.008

# SPC constants for subgroup size 5
A2, D3, D4 = 0.577, 0.0, 2.114

measurements = np.random.normal(target, process_std, (n_samples, subgroup_size))

# OOC signals at samples 9/17/26 — down/up/down pattern (different from sibling impls)
measurements[8] -= 0.030  # sample 9: below LCL
measurements[16] += 0.027  # sample 17: above UCL
measurements[25] -= 0.028  # sample 26: below LCL

sample_means = measurements.mean(axis=1)
sample_ranges = measurements.max(axis=1) - measurements.min(axis=1)

# Control limits — X-bar chart
xbar_bar = sample_means.mean()
r_bar = sample_ranges.mean()
xbar_ucl = xbar_bar + A2 * r_bar
xbar_lcl = xbar_bar - A2 * r_bar
xbar_upper_warn = xbar_bar + (2 / 3) * A2 * r_bar
xbar_lower_warn = xbar_bar - (2 / 3) * A2 * r_bar

# Control limits — R chart
r_ucl = D4 * r_bar
r_lcl = D3 * r_bar
r_upper_warn = r_bar + (2 / 3) * (r_ucl - r_bar)
r_lower_warn = max(0.0, r_bar - (2 / 3) * (r_bar - r_lcl))

sample_ids = np.arange(1, n_samples + 1)

xbar_ooc = (sample_means > xbar_ucl) | (sample_means < xbar_lcl)
r_ooc = (sample_ranges > r_ucl) | (sample_ranges < r_lcl)

# Build tidy DataFrame — seaborn-idiomatic long format for FacetGrid
df = pd.DataFrame(
    {
        "Sample": np.tile(sample_ids, 2),
        "Value": np.concatenate([sample_means, sample_ranges]),
        "Chart": np.repeat(["X̄ Chart", "R Chart"], n_samples),
        "OOC": np.concatenate([xbar_ooc, r_ooc]),
    }
)

# FacetGrid — seaborn's multi-panel layout API (dual-panel, shared x-axis)
g = sns.FacetGrid(
    df, row="Chart", row_order=["X̄ Chart", "R Chart"], height=2.25, aspect=8 / 2.25, sharex=True, sharey=False
)
g.figure.set_dpi(400)
g.figure.patch.set_facecolor(PAGE_BG)

# Map data lines via seaborn's map_dataframe
g.map_dataframe(
    sns.lineplot, x="Sample", y="Value", color=BRAND, linewidth=2.5, marker="o", markersize=5, zorder=3, errorbar=None
)


# Map OOC markers via sns.scatterplot for out-of-control detection
def plot_ooc(data, **kwargs):
    ax = plt.gca()
    ooc = data[data["OOC"]]
    if not ooc.empty:
        sns.scatterplot(
            data=ooc,
            x="Sample",
            y="Value",
            ax=ax,
            color=OOC_COLOR,
            s=120,
            zorder=5,
            marker="D",
            edgecolor=PAGE_BG,
            linewidth=1.5,
            legend=False,
        )


g.map_dataframe(plot_ooc)

# Suppress default FacetGrid row labels (added manually as ax.text below)
g.set_titles(template="")

# Per-panel control limits, labels, and annotations
panels = [
    (g.axes[0, 0], "X̄ Chart", xbar_bar, xbar_ucl, xbar_lcl, xbar_upper_warn, xbar_lower_warn, "Sample Mean (mm)"),
    (g.axes[1, 0], "R Chart", r_bar, r_ucl, r_lcl, r_upper_warn, r_lower_warn, "Sample Range (mm)"),
]

for ax, label, cl, ucl, lcl, uw, lw, ylabel in panels:
    ax.set_facecolor(PAGE_BG)
    ax.fill_between(sample_ids, lw, uw, alpha=0.11, color=BRAND, zorder=1)
    ax.axhline(cl, color=BLUE, linewidth=2, label=f"CL = {cl:.4f}")
    ax.axhline(ucl, color=OOC_COLOR, linewidth=1.5, linestyle="--", label=f"UCL = {ucl:.4f}")
    ax.axhline(lcl, color=OOC_COLOR, linewidth=1.5, linestyle="--", label=f"LCL = {lcl:.4f}")
    ax.axhline(uw, color=WARN_COLOR, linewidth=1, linestyle=":", label=f"+2σ = {uw:.4f}")
    ax.axhline(lw, color=WARN_COLOR, linewidth=1, linestyle=":", label=f"−2σ = {lw:.4f}")
    ax.set_ylabel(ylabel, fontsize=10, color=INK)
    ax.legend(fontsize=8, loc="upper right", framealpha=0.9)
    ax.tick_params(axis="both", labelsize=8)
    ax.text(0.01, 0.95, label, transform=ax.transAxes, fontsize=10, fontweight="bold", va="top", color=INK)
    ax.yaxis.grid(True, alpha=0.12, linewidth=0.8)

# Main title on top panel; x-axis label on bottom panel only
title = "spc-xbar-r · python · seaborn · anyplot.ai"
g.axes[0, 0].set_title(title, fontsize=12, fontweight="medium", pad=10, color=INK)
g.axes[1, 0].set_xlabel("Sample Number", fontsize=10, color=INK)
g.axes[0, 0].set_xlabel("")

# Spine cleanup and canvas finalization — 3200×1800 px (figsize=(8,4.5), dpi=400)
sns.despine(fig=g.figure, top=True, right=True)
g.figure.set_size_inches(8, 4.5)
plt.subplots_adjust(hspace=0.12, left=0.11, right=0.97, top=0.92, bottom=0.11)

# Save
plt.savefig(f"plot-{THEME}.png", dpi=400, facecolor=PAGE_BG)
plt.close()

Part of Statistical Process Control Chart (X-bar/R) on anyplot.ai.

Other implementations