Basic Ridgeline Plot — Makie.jl

A ridgeline plot (also known as Joy Plot, named after the Joy Division album cover) displays the distribution of multiple groups by stacking partially overlapping density curves vertically. This creates a mountain ridge appearance that allows efficient comparison of many distributions simultaneously while maintaining a compact and visually striking presentation.

Basic Ridgeline Plot rendered with Makie.jl

Renders

Julia source (Makie.jl)

# anyplot.ai
# ridgeline-basic: Basic Ridgeline Plot
# Library: makie 0.21.9 | Julia 1.11.9
# Quality: 95/100 | Created: 2026-07-25

using CairoMakie
using Colors
using Random
using Statistics

Random.seed!(42)

# --- Theme tokens ------------------------------------------------------------
THEME    = get(ENV, "ANYPLOT_THEME", "light")
PAGE_BG  = THEME == "light" ? colorant"#FAF8F1" : colorant"#1A1A17"
INK      = THEME == "light" ? colorant"#1A1A17" : colorant"#F0EFE8"
INK_SOFT = THEME == "light" ? colorant"#4A4A44" : colorant"#B8B7B0"

# Imprint sequential colormap (brand green -> blue) — the 12 ridges form a
# seasonal cycle, so a gradient reads better here than 12 discrete hues.
IMPRINT_SEQ = cgrad([colorant"#009E73", colorant"#4467A3"])

# --- Data ----------------------------------------------------------------------
# Daily mean temperature normals for a temperate climate (approx. Berlin, DE)
months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
          "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
mean_temp = [1.6, 2.5, 6.1, 10.4, 14.6, 17.9, 19.8, 19.4, 14.9, 10.2, 5.5, 2.5]
std_temp = [4.0, 4.0, 3.7, 3.3, 3.0, 2.7, 2.5, 2.5, 2.8, 3.2, 3.6, 3.9]
n_obs = 250

# Most months are a single symmetric Gaussian; Jan and Jul get an extra
# minority component (a cold-snap / heat-spike bump) so the ridgeline also
# demonstrates shape differences, not just shift/spread differences.
n_bump = 40
daily_temps = Vector{Vector{Float64}}(undef, 12)
for m in 1:12
    main = randn(n_obs - (m in (1, 7) ? n_bump : 0)) .* std_temp[m] .+ mean_temp[m]
    if m == 1
        cold_snap = randn(n_bump) .* (std_temp[m] * 0.5) .+ (mean_temp[m] - 9.0)
        daily_temps[m] = vcat(main, cold_snap)
    elseif m == 7
        heat_spike = randn(n_bump) .* (std_temp[m] * 0.5) .+ (mean_temp[m] + 7.5)
        daily_temps[m] = vcat(main, heat_spike)
    else
        daily_temps[m] = main
    end
end

# Shared temperature grid for the kernel density estimate
grid_min = minimum(minimum.(daily_temps)) - 3.0
grid_max = maximum(maximum.(daily_temps)) + 3.0
temp_grid = range(grid_min, grid_max; length = 300)

# Gaussian KDE per month (Silverman bandwidth), each normalized to a shared
# ridge height so the vertical overlap stays consistent across all 12 ridges
ridge_step = 1.0
ridge_height = 2.3
baselines = [(m - 1) * ridge_step for m in 1:12]
ridge_curves = Vector{Vector{Float64}}(undef, 12)
for m in 1:12
    data = daily_temps[m]
    bandwidth = 1.06 * std(data) * length(data)^(-1 / 5)
    density = [sum(exp.(-0.5 .* ((t .- data) ./ bandwidth) .^ 2)) for t in temp_grid]
    density ./= (length(data) * bandwidth * sqrt(2π))
    ridge_curves[m] = density ./ maximum(density) .* ridge_height .+ baselines[m]
end

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

ax = Axis(
    fig[1, 1];
    title = "ridgeline-basic · julia · makie · anyplot.ai",
    titlesize = 20,
    titlecolor = INK,
    xlabel = "Temperature (°C)",
    xlabelsize = 16,
    xlabelcolor = INK,
    xticklabelsize = 13,
    xticklabelcolor = INK_SOFT,
    xtickcolor = INK_SOFT,
    yticks = (baselines, months),
    yticklabelsize = 13,
    yticklabelcolor = INK_SOFT,
    yticksvisible = false,
    backgroundcolor = PAGE_BG,
    topspinevisible = false,
    rightspinevisible = false,
    leftspinevisible = false,
    bottomspinecolor = INK_SOFT,
    xgridcolor = RGBAf(INK.r, INK.g, INK.b, 0.15),
    ygridvisible = false,
)

# Color keyed to actual mean temperature (min-max normalized), not month
# index, so the gradient reinforces the warm/cold story instead of just
# re-encoding the chronological order the Y-axis already shows.
temp_norm = (mean_temp .- minimum(mean_temp)) ./ (maximum(mean_temp) - minimum(mean_temp))

for m in 12:-1:1
    ridge_color = get(IMPRINT_SEQ, temp_norm[m])
    band!(ax, temp_grid, fill(baselines[m], length(temp_grid)), ridge_curves[m];
          color = ridge_color)
    lines!(ax, temp_grid, ridge_curves[m]; color = INK, linewidth = 1.3)
end

ylims!(ax, -0.5, baselines[end] + ridge_height + 0.4)
xlims!(ax, grid_min, grid_max)

# --- 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/ridgeline-basic/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": "ridgeline-basic",
  "language": "julia",
  "library": "makie",
  "page": "https://anyplot.ai/ridgeline-basic/julia/makie",
  "hub": "https://anyplot.ai/ridgeline-basic",
  "code_json": "https://api.anyplot.ai/specs/ridgeline-basic/makie/code",
  "spec_json": "https://api.anyplot.ai/specs/ridgeline-basic",
  "render_light_png": "https://storage.googleapis.com/anyplot-images/plots/ridgeline-basic/julia/makie/plot-light.png",
  "render_dark_png": "https://storage.googleapis.com/anyplot-images/plots/ridgeline-basic/julia/makie/plot-dark.png",
  "quality_score": 95.0,
  "license": "MIT",
  "guide": "https://anyplot.ai/llms.txt"
}

Part of Basic Ridgeline Plot on anyplot.ai.

Other implementations