Residual Plot in Makie.jl (Julia)

The same plot in 14 other libraries — Python: Altair, Bokeh, lets-plot, Matplotlib, Plotly, plotnine, Pygal, Seaborn; R: ggplot2; JavaScript: Chart.js, D3.js, Apache ECharts, Highcharts, MUI X Charts. Compare all 15 side by side: Residual Plot in Python, R, Julia and JavaScript.

A residual plot displays the difference between observed and predicted values (residuals) against fitted values in regression analysis. This diagnostic visualization helps identify violations of regression assumptions including non-linearity, heteroscedasticity (non-constant variance), and outliers. A well-fitting model shows residuals randomly scattered around zero with no discernible pattern.

Residual Plot rendered with Makie.jl

Renders

Julia source (Makie.jl)

# anyplot.ai
# residual-plot: Residual Plot
# Library: makie 0.21.9 | Julia 1.11.9
# Quality: 94/100 | Created: 2026-09-05

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 INK      = THEME == "light" ? colorant"#1A1A17" : colorant"#F0EFE8"
const INK_SOFT = THEME == "light" ? colorant"#4A4A44" : colorant"#B8B7B0"
const MUTED    = THEME == "light" ? colorant"#6B6A63" : colorant"#A8A79F"
const IMPRINT_PALETTE = [
    colorant"#009E73", colorant"#C475FD", colorant"#4467A3", colorant"#BD8233",
    colorant"#AE3030", colorant"#2ABCCD", colorant"#954477", colorant"#99B314",
]

# --- Data ---------------------------------------------------------------
# Advertising spend ($1000s) predicting weekly revenue ($1000s). The true
# relationship is linear but noise variance grows with spend, so a simple
# OLS fit leaves heteroscedastic residuals — exactly what this plot is
# meant to expose.
n = 300
ad_spend = sort(rand(n) .* 190 .+ 10)
noise_scale = 1.0 .+ 0.06 .* ad_spend
revenue = 50 .+ 4.5 .* ad_spend .+ randn(n) .* noise_scale .* 3.5

spend_mean = mean(ad_spend)
revenue_mean = mean(revenue)
slope = sum((ad_spend .- spend_mean) .* (revenue .- revenue_mean)) / sum((ad_spend .- spend_mean) .^ 2)
intercept = revenue_mean - slope * spend_mean
fitted_revenue = intercept .+ slope .* ad_spend
residuals = revenue .- fitted_revenue

resid_std = std(residuals)
is_outlier = abs.(residuals) .> 2 * resid_std

# Quantify the heteroscedasticity story: local spread in the narrow (low
# fitted-revenue) and wide (high fitted-revenue) quartiles, called out
# directly on the plot instead of leaving the funnel shape to speak for itself.
lo_cut = quantile(fitted_revenue, 0.25)
hi_cut = quantile(fitted_revenue, 0.75)
lo_mask = fitted_revenue .< lo_cut
hi_mask = fitted_revenue .> hi_cut
lo_sigma = std(residuals[lo_mask])
hi_sigma = std(residuals[hi_mask])
lo_anchor_x = quantile(fitted_revenue[lo_mask], 0.5)
hi_anchor_x = quantile(fitted_revenue[hi_mask], 0.5)
lo_anchor_y = maximum(residuals[lo_mask])
hi_anchor_y = maximum(residuals[hi_mask])

idx_max = argmax(residuals)
idx_min = argmin(residuals)

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

ax = Axis(
    fig[1, 1];
    title              = "residual-plot · julia · makie · anyplot.ai",
    titlesize          = 20,
    titlecolor         = INK,
    subtitle           = "Residual spread widens with fitted revenue — a classic heteroscedasticity signature",
    subtitlesize       = 13,
    subtitlecolor      = INK_SOFT,
    xlabel             = "Fitted Revenue (\$1000s)",
    ylabel             = "Residual (\$1000s)",
    xlabelsize         = 14,
    ylabelsize         = 14,
    xlabelcolor        = INK,
    ylabelcolor        = INK,
    xticklabelsize     = 12,
    yticklabelsize     = 12,
    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,
    xgridcolor         = RGBAf(INK.r, INK.g, INK.b, 0.15),
    ygridcolor         = RGBAf(INK.r, INK.g, INK.b, 0.15),
    xminorgridvisible  = false,
    yminorgridvisible  = false,
)

hspan!(ax, -2 * resid_std, 2 * resid_std; color = RGBAf(MUTED.r, MUTED.g, MUTED.b, 0.10))
hlines!(ax, [-2resid_std, 2resid_std]; color = MUTED, linewidth = 1.5, linestyle = :dot)
hlines!(ax, [0]; color = INK_SOFT, linewidth = 2.5, linestyle = :dash)

scatter!(
    ax, fitted_revenue[.!is_outlier], residuals[.!is_outlier];
    color = IMPRINT_PALETTE[1], markersize = 11, alpha = 0.65, strokewidth = 0,
    label = "Residual",
)
scatter!(
    ax, fitted_revenue[is_outlier], residuals[is_outlier];
    color = IMPRINT_PALETTE[5], markersize = 14, strokewidth = 1, strokecolor = PAGE_BG,
    label = "Outlier (|residual| > 2σ)",
)

# Quantified spread callouts anchored directly in data coordinates — makes the
# heteroscedasticity insight explicit rather than leaving the funnel to speak
# for itself.
text!(
    ax, lo_anchor_x, lo_anchor_y;
    text = "SD ≈ $(round(lo_sigma, digits = 1))", color = INK, fontsize = 13,
    font = :bold, align = (:center, :bottom), offset = (0, 10),
)
text!(
    ax, hi_anchor_x, hi_anchor_y;
    text = "SD ≈ $(round(hi_sigma, digits = 1))", color = INK, fontsize = 13,
    font = :bold, align = (:center, :bottom), offset = (0, 10),
)

# Label the two most extreme outliers to give the outlier cluster a
# deliberate focal point instead of leaving it as undifferentiated dots.
text!(
    ax, fitted_revenue[idx_max], residuals[idx_max];
    text = "largest overshoot: +$(round(Int, residuals[idx_max]))",
    color = IMPRINT_PALETTE[5], fontsize = 12, font = :bold,
    align = (:left, :bottom), offset = (12, 8),
)
text!(
    ax, fitted_revenue[idx_min], residuals[idx_min];
    text = "largest undershoot: $(round(Int, residuals[idx_min]))",
    color = IMPRINT_PALETTE[5], fontsize = 12, font = :bold,
    align = (:left, :top), offset = (12, -8),
)

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

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

Retrieve this implementation

Runnable source as JSON, for any HTTP client: https://api.anyplot.ai/specs/residual-plot/makie/code. Any spec id and library id listed in llms-full.txt fit the same URL shape; every URL below is complete and callable.

{
  "spec_id": "residual-plot",
  "language": "julia",
  "library": "makie",
  "page": "https://anyplot.ai/residual-plot/julia/makie",
  "hub": "https://anyplot.ai/residual-plot",
  "code_json": "https://api.anyplot.ai/specs/residual-plot/makie/code",
  "spec_json": "https://api.anyplot.ai/specs/residual-plot",
  "render_light_png": "https://storage.googleapis.com/anyplot-images/plots/residual-plot/julia/makie/plot-light.png",
  "render_dark_png": "https://storage.googleapis.com/anyplot-images/plots/residual-plot/julia/makie/plot-dark.png",
  "quality_score": 94.0,
  "license": "MIT",
  "guide": "https://anyplot.ai/llms.txt"
}

Part of Residual Plot on anyplot.ai.

Other implementations