Basic ECDF Plot — Makie.jl

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 Makie.jl

Renders

Julia source (Makie.jl)

# anyplot.ai
# ecdf-basic: Basic ECDF Plot
# Library: makie 0.21.9 | Julia 1.11.9
# Quality: 88/100 | Created: 2026-06-25

using CairoMakie
using Colors
using Random
using Statistics

Random.seed!(42)

# Theme tokens
const THEME       = get(ENV, "ANYPLOT_THEME", "light")
const PAGE_BG     = THEME == "light" ? colorant"#FAF8F1" : colorant"#1A1A17"
const ELEVATED_BG = THEME == "light" ? colorant"#FFFDF6" : colorant"#242420"
const INK         = THEME == "light" ? colorant"#1A1A17" : colorant"#F0EFE8"
const INK_SOFT    = THEME == "light" ? colorant"#4A4A44" : colorant"#B8B7B0"
const IMPRINT_PALETTE = [
    colorant"#009E73",
    colorant"#C475FD",
    colorant"#4467A3",
    colorant"#BD8233",
    colorant"#AE3030",
    colorant"#2ABCCD",
    colorant"#954477",
    colorant"#99B314",
]

# Data: page load times (seconds) for three server configurations (n=200 each)
n = 200
times_cached = randn(n) .* 0.20 .+ 0.80
times_cdn    = randn(n) .* 0.30 .+ 1.40
times_legacy = randn(n) .* 0.60 .+ 2.80

# P50 (median) for each group — used for reference lines
p50_c   = median(times_cached)
p50_cdn = median(times_cdn)
p50_l   = median(times_legacy)

# Compute ECDFs: sort data, build cumulative proportions, extend tails for clean drawing
sorted_c   = sort(times_cached)
ext_c      = 0.08 * (sorted_c[end] - sorted_c[1])
xs_c       = vcat([sorted_c[1] - ext_c], sorted_c, [sorted_c[end] + ext_c])
ys_c       = vcat([0.0], collect(1:n) ./ n, [1.0])

sorted_cdn = sort(times_cdn)
ext_cdn    = 0.08 * (sorted_cdn[end] - sorted_cdn[1])
xs_cdn     = vcat([sorted_cdn[1] - ext_cdn], sorted_cdn, [sorted_cdn[end] + ext_cdn])
ys_cdn     = vcat([0.0], collect(1:n) ./ n, [1.0])

sorted_l   = sort(times_legacy)
ext_l      = 0.08 * (sorted_l[end] - sorted_l[1])
xs_l       = vcat([sorted_l[1] - ext_l], sorted_l, [sorted_l[end] + ext_l])
ys_l       = vcat([0.0], collect(1:n) ./ n, [1.0])

# Plot
fig = Figure(
    size            = (1600, 900),
    fontsize        = 14,
    backgroundcolor = PAGE_BG,
)

ax = Axis(
    fig[1, 1];
    title              = "ecdf-basic · julia · makie · anyplot.ai",
    titlesize          = 22,
    titlecolor         = INK,
    xlabel             = "Page Load Time (seconds)",
    ylabel             = "Cumulative Proportion",
    xlabelsize         = 14,
    ylabelsize         = 14,
    xticklabelsize     = 12,
    yticklabelsize     = 12,
    xlabelcolor        = INK,
    ylabelcolor        = INK,
    xticklabelcolor    = INK_SOFT,
    yticklabelcolor    = INK_SOFT,
    xtickcolor         = INK_SOFT,
    ytickcolor         = INK_SOFT,
    backgroundcolor    = PAGE_BG,
    topspinevisible    = false,
    rightspinevisible  = false,
    leftspinecolor     = INK_SOFT,
    bottomspinecolor   = INK_SOFT,
    xgridvisible       = false,
    ygridcolor         = RGBAf(INK.r, INK.g, INK.b, 0.15),
    xminorgridvisible  = false,
    yminorgridvisible  = false,
    yticks             = 0.0:0.1:1.0,
)

# Horizontal reference at P50 (y=0.5) — reads off median directly
hlines!(ax, [0.5];
    color     = RGBAf(INK_SOFT.r, INK_SOFT.g, INK_SOFT.b, 0.30),
    linestyle = :dash,
    linewidth = 1.0,
)

# Vertical drop lines at each group's P50
vlines!(ax, [p50_c];
    color     = RGBAf(IMPRINT_PALETTE[1].r, IMPRINT_PALETTE[1].g, IMPRINT_PALETTE[1].b, 0.45),
    linestyle = :dash,
    linewidth = 1.2,
)
vlines!(ax, [p50_cdn];
    color     = RGBAf(IMPRINT_PALETTE[2].r, IMPRINT_PALETTE[2].g, IMPRINT_PALETTE[2].b, 0.45),
    linestyle = :dash,
    linewidth = 1.2,
)
vlines!(ax, [p50_l];
    color     = RGBAf(IMPRINT_PALETTE[3].r, IMPRINT_PALETTE[3].g, IMPRINT_PALETTE[3].b, 0.45),
    linestyle = :dash,
    linewidth = 1.2,
)

# ECDF step curves
stairs!(ax, xs_c, ys_c;
    step      = :post,
    color     = IMPRINT_PALETTE[1],
    linewidth = 2.5,
    label     = "Cached Server",
)
stairs!(ax, xs_cdn, ys_cdn;
    step      = :post,
    color     = IMPRINT_PALETTE[2],
    linewidth = 2.5,
    label     = "CDN Server",
)
stairs!(ax, xs_l, ys_l;
    step      = :post,
    color     = IMPRINT_PALETTE[3],
    linewidth = 2.5,
    label     = "Legacy Server",
)

# P50 callout annotations — label each median crossing
text!(ax, p50_c,   0.53;
    text      = "P50: $(round(p50_c,   digits=2))s",
    color     = IMPRINT_PALETTE[1],
    fontsize  = 10,
    align     = (:left, :bottom),
)
text!(ax, p50_cdn, 0.53;
    text      = "P50: $(round(p50_cdn, digits=2))s",
    color     = IMPRINT_PALETTE[2],
    fontsize  = 10,
    align     = (:left, :bottom),
)
text!(ax, p50_l,   0.53;
    text      = "P50: $(round(p50_l,   digits=2))s",
    color     = IMPRINT_PALETTE[3],
    fontsize  = 10,
    align     = (:left, :bottom),
)

ylims!(ax, -0.02, 1.05)

axislegend(ax;
    position        = :lt,
    backgroundcolor = ELEVATED_BG,
    labelcolor      = INK_SOFT,
    framevisible    = false,
    labelsize       = 12,
)

# Save
save("plot-$(THEME).png", fig; px_per_unit = 2)

Part of Basic ECDF Plot on anyplot.ai.

Other implementations