Basic Q-Q Plot — Pygal

A Q-Q (Quantile-Quantile) plot compares the distribution of a dataset against a theoretical distribution (typically normal) or another dataset. Points are plotted by matching sample quantiles to theoretical quantiles, with a diagonal reference line indicating perfect distribution match. Deviations from the line reveal distribution characteristics such as skewness, heavy tails, and outliers.

Basic Q-Q Plot rendered with Pygal

Python source (Pygal)

""" anyplot.ai
qq-basic: Basic Q-Q Plot
Library: pygal 3.1.3 | Python 3.13.14
Quality: 92/100 | Updated: 2026-07-24
"""

import sys


sys.path.pop(0)  # prevent this file from shadowing the installed pygal package

import os
from statistics import NormalDist

import numpy as np
import pygal
from pygal.style import Style


THEME = os.getenv("ANYPLOT_THEME", "light")
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F"

BRAND = "#009E73"  # Imprint palette position 1 — always first series
AMBER = "#DDCC77"  # Imprint semantic anchor — warning / caution, flags the slow-response tail

# Data - API response times (ms): a fast, tightly-clustered core plus an
# occasional slow request, producing the right-skewed tail that QQ plots
# are commonly used to surface before assuming normality in latency analysis.
np.random.seed(42)
baseline = np.random.normal(150, 18, 80)
slow_tail = 150 + np.random.exponential(35, 20)
sample = np.sort(np.concatenate([baseline, slow_tail]))
n = len(sample)

# Calculate theoretical quantiles using standard library
_nd = NormalDist()
probabilities = (np.arange(1, n + 1) - 0.5) / n
theoretical_quantiles = np.array([_nd.inv_cdf(float(p)) for p in probabilities])

# Theoretical quantiles are unitless z-scores while the sample is in ms, so
# the fitted reference line uses the sample's own mean/std (y = mean + std*x)
# rather than y=x — the standard QQ convention when axes are in different units
# (matches scipy.stats.probplot / statsmodels qqplot). The legend label spells
# this out explicitly so it doesn't read as a spec deviation.
theo_margin = (theoretical_quantiles.max() - theoretical_quantiles.min()) * 0.1
line_x_min = theoretical_quantiles.min() - theo_margin
line_x_max = theoretical_quantiles.max() + theo_margin

sample_mean = sample.mean()
sample_std = sample.std()

# Flag the slow-response tail (>2 std above the mean) so the deviation this
# QQ plot exists to reveal is visually called out, not just implied by shape.
# Split into two series (rather than per-point color overrides) so the
# outliers get their own legend swatch — self-explanatory in the static PNG.
outlier_threshold = sample_mean + 2 * sample_std
paired = list(zip(theoretical_quantiles, sample, strict=True))
normal_points = [(tq, s) for tq, s in paired if s <= outlier_threshold]
outlier_points = [(tq, s) for tq, s in paired if s > outlier_threshold]

custom_style = Style(
    background=PAGE_BG,
    plot_background=PAGE_BG,
    foreground=INK,
    foreground_strong=INK,
    foreground_subtle=INK_MUTED,
    # series 1 = brand green, series 2 = amber outlier anchor, series 3 (reference line) = neutral ink
    colors=(BRAND, AMBER, INK),
    title_font_size=66,
    label_font_size=56,
    major_label_font_size=44,
    legend_font_size=44,
    value_font_size=36,
    stroke_width=2.5,
)

chart = pygal.XY(
    width=3200,
    height=1800,
    style=custom_style,
    title="qq-basic · python · pygal · anyplot.ai",
    x_title="Theoretical Quantiles",
    y_title="Sample Quantiles (ms)",
    show_legend=True,
    legend_at_bottom=True,
    dots_size=13,
    stroke=False,
    show_x_guides=False,
    show_y_guides=True,
)

chart.add("API Response Time", normal_points)
chart.add("Outlier (>2σ)", outlier_points)

reference_line = [
    (line_x_min, sample_mean + sample_std * line_x_min),
    (line_x_max, sample_mean + sample_std * line_x_max),
]
chart.add("Reference (Normal Fit)", reference_line, stroke=True, show_dots=False, dots_size=0)

chart.render_to_png(f"plot-{THEME}.png")
with open(f"plot-{THEME}.html", "wb") as f:
    f.write(chart.render())

Part of Basic Q-Q Plot on anyplot.ai.

Other implementations