Basic ECDF Plot — plotnine

An ECDF (Empirical Cumulative Distribution Function) plot displays a step function that shows the proportion of observations less than or equal to each value. Unlike histograms, ECDF plots require no binning or smoothing, providing a non-parametric estimate of the cumulative distribution. The y-axis ranges from 0 to 1, allowing direct reading of percentiles and quantiles from the visualization.

Basic ECDF Plot rendered with plotnine

Python source (plotnine)

""" anyplot.ai
ecdf-basic: Basic ECDF Plot
Library: plotnine 0.15.7 | Python 3.13.14
Quality: 89/100 | Updated: 2026-06-25
"""

import os
import sys

import numpy as np
import pandas as pd


# Avoid shadowing the plotnine library when this file is run directly
_cwd = os.getcwd()
sys.path = [p for p in sys.path if os.path.abspath(p) != _cwd]

from plotnine import (
    aes,
    annotate,
    element_blank,
    element_line,
    element_rect,
    element_text,
    geom_hline,
    geom_vline,
    ggplot,
    ggsave,
    labs,
    scale_x_continuous,
    scale_y_continuous,
    stat_ecdf,
    theme,
    theme_minimal,
)


# Theme tokens (Imprint palette)
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"
BRAND = "#009E73"  # Imprint palette position 1

# Data: test scores — normal distribution
np.random.seed(42)
scores = np.random.randn(200) * 15 + 50
q1 = float(np.percentile(scores, 25))
median_score = float(np.median(scores))
q3 = float(np.percentile(scores, 75))

df = pd.DataFrame({"score": scores})

title = "ecdf-basic · python · plotnine · anyplot.ai"

# Plot — stat_ecdf computes ECDF internally via plotnine's stat layer
plot = (
    ggplot(df, aes(x="score"))
    # IQR shaded band — highlights the interquartile range (middle 50%)
    + annotate("rect", xmin=q1, xmax=q3, ymin=0.0, ymax=1.0, fill=BRAND, alpha=0.06)
    # Quartile reference crosshairs (Q1, median, Q3)
    + geom_hline(yintercept=0.25, color=INK_SOFT, size=0.5, linetype="dotted", alpha=0.55)
    + geom_hline(yintercept=0.50, color=INK_SOFT, size=0.6, linetype="dotted", alpha=0.70)
    + geom_hline(yintercept=0.75, color=INK_SOFT, size=0.5, linetype="dotted", alpha=0.55)
    + geom_vline(xintercept=q1, color=INK_SOFT, size=0.5, linetype="dotted", alpha=0.55)
    + geom_vline(xintercept=median_score, color=INK_SOFT, size=0.6, linetype="dotted", alpha=0.70)
    + geom_vline(xintercept=q3, color=INK_SOFT, size=0.5, linetype="dotted", alpha=0.55)
    # ECDF step line — rendered on top of reference elements
    + stat_ecdf(geom="step", color=BRAND, size=1.1)
    # Quartile annotations — teach readers how to extract percentile information
    + annotate("text", x=q1 + 1.5, y=0.16, label=f"Q1: {q1:.1f}", color=INK_SOFT, size=3.5, ha="left")
    + annotate(
        "text", x=median_score + 1.5, y=0.08, label=f"Median: {median_score:.1f}", color=INK_SOFT, size=4.0, ha="left"
    )
    + annotate("text", x=q3 + 1.5, y=0.80, label=f"Q3: {q3:.1f}", color=INK_SOFT, size=3.5, ha="left")
    + labs(x="Test Score (points)", y="Cumulative Proportion", title=title)
    + scale_x_continuous(expand=(0.01, 0))
    + scale_y_continuous(limits=(0, 1), breaks=np.arange(0, 1.1, 0.1), expand=(0.01, 0))
    + theme_minimal()
    + theme(
        figure_size=(8, 4.5),
        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),
        panel_background=element_rect(fill=PAGE_BG, color=PAGE_BG),
        panel_border=element_blank(),
        panel_grid_major=element_line(color=INK, size=0.3, alpha=0.15),
        panel_grid_minor=element_blank(),
        axis_line=element_line(color=INK_SOFT, size=0.6),
        axis_ticks=element_line(color=INK_SOFT, size=0.5),
        text=element_text(color=INK, size=7),
        plot_title=element_text(color=INK, size=12, weight="medium", ha="left"),
        axis_title=element_text(color=INK, size=10),
        axis_text=element_text(color=INK_SOFT, size=8),
        legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),
        legend_text=element_text(color=INK_SOFT, size=8),
        legend_title=element_text(color=INK, size=8),
    )
)

ggsave(plot, filename=f"plot-{THEME}.png", dpi=400, width=8, height=4.5)

Part of Basic ECDF Plot on anyplot.ai.

Other implementations