Basic ECDF Plot — Plotly

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 Plotly

Python source (Plotly)

""" anyplot.ai
ecdf-basic: Basic ECDF Plot
Library: plotly 6.8.0 | Python 3.13.14
Quality: 88/100 | Updated: 2026-06-25
"""

import os
import sys


# Remove the script's own directory from sys.path so this file doesn't shadow
# the installed plotly package (script is named plotly.py).
sys.path = [p for p in sys.path if p not in ("", os.path.dirname(os.path.abspath(__file__)))]

import numpy as np
import plotly.graph_objects as go


# Theme tokens
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"
GRID = "rgba(26,26,23,0.15)" if THEME == "light" else "rgba(240,239,232,0.15)"
BRAND = "#009E73"  # Imprint palette position 1 — always first series
ANYPLOT_AMBER = "#DDCC77"  # annotation accent

# Data: HTTP API response latencies (ms) for 400 requests to a web service
np.random.seed(42)
n_requests = 400
# Lognormal distribution models real-world latency well (long right tail)
latency_ms = np.random.lognormal(mean=4.8, sigma=0.5, size=n_requests)

# ECDF
sorted_latency = np.sort(latency_ms)
cumulative_proportion = np.arange(1, n_requests + 1) / n_requests

# Key percentiles
p50 = np.percentile(sorted_latency, 50)
p90 = np.percentile(sorted_latency, 90)
p99 = np.percentile(sorted_latency, 99)
x_max = np.percentile(sorted_latency, 99.5)

# Plot
fig = go.Figure()

# ECDF line
fig.add_trace(
    go.Scatter(
        x=sorted_latency,
        y=cumulative_proportion,
        mode="lines",
        line={"color": BRAND, "width": 2.5, "shape": "hv"},
        hovertemplate="<b>Latency</b>: %{x:.1f} ms<br><b>Cumulative</b>: %{y:.1%}<extra></extra>",
        showlegend=False,
    )
)

# Percentile reference lines (dashed horizontal)
for pct_val, pct_label, y_pct in [(p50, "P50", 0.50), (p90, "P90", 0.90), (p99, "P99", 0.99)]:
    fig.add_shape(
        type="line",
        x0=0,
        x1=pct_val,
        y0=y_pct,
        y1=y_pct,
        line={"color": INK_SOFT, "width": 1, "dash": "dot"},
        layer="below",
    )
    fig.add_shape(
        type="line",
        x0=pct_val,
        x1=pct_val,
        y0=0,
        y1=y_pct,
        line={"color": INK_SOFT, "width": 1, "dash": "dot"},
        layer="below",
    )
    fig.add_annotation(
        x=pct_val,
        y=y_pct,
        text=f"<b>{pct_label}</b> {pct_val:.0f} ms",
        showarrow=False,
        xanchor="left",
        yanchor="bottom",
        xshift=6,
        yshift=4,
        font={"size": 10, "color": INK_SOFT},
    )

# Style
title_text = "HTTP API Latency · ecdf-basic · python · plotly · anyplot.ai"
fig.update_layout(
    autosize=False,
    title={"text": title_text, "font": {"size": 16, "color": INK}, "x": 0.5, "xanchor": "center", "y": 0.95},
    xaxis={
        "title": {"text": "Response Time (ms)", "font": {"size": 12, "color": INK}, "standoff": 12},
        "tickfont": {"size": 10, "color": INK_SOFT},
        "gridcolor": GRID,
        "gridwidth": 1,
        "linecolor": INK_SOFT,
        "zeroline": False,
        "showline": False,
        "mirror": False,
        "range": [0, x_max],
    },
    yaxis={
        "title": {"text": "Cumulative Proportion of Requests", "font": {"size": 12, "color": INK}, "standoff": 12},
        "tickfont": {"size": 10, "color": INK_SOFT},
        "gridcolor": GRID,
        "gridwidth": 1,
        "linecolor": INK_SOFT,
        "zeroline": False,
        "showline": False,
        "mirror": False,
        "range": [0, 1.02],
        "tickformat": ".0%",
        "dtick": 0.1,
    },
    paper_bgcolor=PAGE_BG,
    plot_bgcolor=PAGE_BG,
    font={"color": INK, "family": "Inter, Helvetica Neue, Arial, sans-serif"},
    margin={"l": 80, "r": 40, "t": 80, "b": 60},
    hovermode="x unified",
    hoverlabel={"bgcolor": ELEVATED_BG, "bordercolor": INK_SOFT, "font": {"color": INK, "size": 12}, "align": "left"},
)

# Save
fig.write_image(f"plot-{THEME}.png", width=800, height=450, scale=4)
fig.write_html(
    f"plot-{THEME}.html",
    include_plotlyjs="cdn",
    config={
        "displaylogo": False,
        "modeBarButtonsToRemove": ["lasso2d", "select2d", "autoScale2d"],
        "toImageButtonOptions": {"format": "png", "filename": "ecdf-basic-plotly"},
    },
)

Part of Basic ECDF Plot on anyplot.ai.

Other implementations