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.

""" anyplot.ai
ecdf-basic: Basic ECDF Plot
Library: pygal 3.1.3 | Python 3.13.14
Quality: 88/100 | Created: 2026-06-25
"""
import os
import sys
# Script filename shadows the installed `pygal` package when run as `python pygal.py`;
# dropping the script directory from sys.path lets the real package resolve.
sys.path.pop(0)
import numpy as np
import pygal
from pygal.style import Style
# Theme tokens
THEME = os.getenv("ANYPLOT_THEME", "light")
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F"
IMPRINT_PALETTE = ("#009E73", "#C475FD", "#4467A3", "#BD8233", "#AE3030", "#2ABCCD", "#954477", "#99B314")
# 120 food-delivery times (minutes); gamma is right-skewed, realistic for
# order-to-door durations (mean ≈ 30 min, long tail for outliers).
np.random.seed(42)
delivery_times = np.random.gamma(shape=6.0, scale=5.0, size=120)
# ECDF: sorted values → cumulative proportion k/n
sorted_values = np.sort(delivery_times)
n = len(sorted_values)
ecdf_y = np.arange(1, n + 1) / n
# Step function: flat lead-in at 0, then a vertical jump + horizontal plateau
# at each observation.
x_lead = float(sorted_values[0]) - 2.0
step_points = [(x_lead, 0.0), (float(sorted_values[0]), 0.0)]
for i in range(n):
step_points.append((float(sorted_values[i]), float(ecdf_y[i])))
x_next = float(sorted_values[i + 1]) if i + 1 < n else float(sorted_values[-1]) + 2.0
step_points.append((x_next, float(ecdf_y[i])))
# Quartile markers for distribution landmarks
p25 = float(np.percentile(delivery_times, 25))
p50 = float(np.percentile(delivery_times, 50))
p75 = float(np.percentile(delivery_times, 75))
font = "DejaVu Sans, Helvetica, Arial, sans-serif"
custom_style = Style(
background=PAGE_BG,
plot_background=PAGE_BG,
foreground=INK_SOFT,
foreground_strong=INK,
foreground_subtle=INK_MUTED,
colors=IMPRINT_PALETTE,
font_family=font,
title_font_family=font,
label_font_family=font,
major_label_font_family=font,
legend_font_family=font,
tooltip_font_family=font,
title_font_size=66,
label_font_size=56,
major_label_font_size=44,
legend_font_size=44,
tooltip_font_size=32,
value_font_size=30,
stroke_opacity=1,
stroke_opacity_hover=1,
opacity=1,
opacity_hover=1,
stroke_width=3,
)
chart = pygal.XY(
width=3200,
height=1800,
style=custom_style,
title="ecdf-basic · python · pygal · anyplot.ai",
x_title="Delivery Time (minutes)",
y_title="Cumulative Proportion",
show_dots=False,
show_x_guides=True,
show_y_guides=True,
show_legend=True,
range=(0, 1.05),
x_labels_major_count=9,
y_labels_major_count=6,
value_formatter=lambda v: f"{v:.2f}",
x_value_formatter=lambda v: f"{v:.0f}",
margin=50,
truncate_legend=-1,
js=[],
legend_at_bottom=True,
legend_at_bottom_columns=2,
)
# ECDF step function — series 1 uses brand green (#009E73)
chart.add("ECDF — Delivery Times", step_points)
# Three quartile markers as separate series so each gets a distinct Imprint color
# and its own legend entry with the exact value, making percentiles directly readable.
chart.add(
f"Q1 = {p25:.0f} min (25th percentile)",
[{"value": (p25, 0.25), "label": f"P25 = {p25:.1f} min"}],
stroke=False,
show_dots=True,
dots_size=20,
)
chart.add(
f"Median = {p50:.0f} min (50th percentile)",
[{"value": (p50, 0.50), "label": f"Median = {p50:.1f} min"}],
stroke=False,
show_dots=True,
dots_size=20,
)
chart.add(
f"Q3 = {p75:.0f} min (75th percentile)",
[{"value": (p75, 0.75), "label": f"P75 = {p75:.1f} min"}],
stroke=False,
show_dots=True,
dots_size=20,
)
chart.render_to_png(f"plot-{THEME}.png")
with open(f"plot-{THEME}.html", "wb") as f:
f.write(chart.render())
Part of Basic ECDF Plot on anyplot.ai.