MA Plot for Differential Expression — Makie.jl

An MA plot (M-versus-A plot) visualizes the relationship between log fold change (M) and mean average expression (A) when comparing two experimental conditions. Each point represents a gene or feature, with significantly differentially expressed genes highlighted. This plot is a standard diagnostic tool in RNA-seq and microarray analysis for assessing differential expression results and detecting systematic expression-dependent bias.

MA Plot for Differential Expression rendered with Makie.jl

Renders

Julia source (Makie.jl)

# anyplot.ai
# ma-differential-expression: MA Plot for Differential Expression
# Library: makie 0.22.10 | Julia 1.11.9
# Quality: 88/100 | Created: 2026-06-21

using CairoMakie
using Colors
using Random

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 INK_MUTED   = THEME == "light" ? colorant"#6B6A63" : colorant"#A8A79F"

# Imprint palette positions used in this plot
const BRAND_GREEN = colorant"#009E73"  # position 1 — up-regulated (gain)
const MATTE_RED   = colorant"#AE3030"  # semantic anchor — down-regulated (loss)
const OCHRE       = colorant"#BD8233"  # position 4 — LOESS trend line

# Data — simulated RNA-seq differential expression (~15 000 genes)
n_genes = 15_000

# A-values: mean log2 expression (funnel-shaped distribution typical of RNA-seq)
mean_expr = abs.(randn(n_genes)) .* 3.5 .+ abs.(randn(n_genes)) .* 1.0
mean_expr = clamp.(mean_expr, 0.05, 14.5)

# M-values: LFC variance shrinks at higher expression
lfc_noise = (2.0 ./ (mean_expr .+ 0.8)) .+ 0.18
lfc = randn(n_genes) .* lfc_noise

# Slight upward bias at low expression — common normalization artifact
lfc .+= 0.25 .* exp.(-mean_expr ./ 3.5)

# Inject ~10 % truly differentially expressed genes
n_de    = 1_500
de_idx  = randperm(n_genes)[1:n_de]
half_de = div(n_de, 2)
for i in 1:half_de
    lfc[de_idx[i]] += 1.9 + abs(randn()) * 0.6
end
for i in (half_de + 1):n_de
    lfc[de_idx[i]] -= 1.9 + abs(randn()) * 0.6
end
lfc = clamp.(lfc, -7.5, 7.5)

significant = falses(n_genes)
significant[de_idx] .= true

not_sig  = .!significant
sig_up   = significant .& (lfc .>  0.0)
sig_down = significant .& (lfc .<= 0.0)

# Gaussian kernel smoothing — LOESS approximation (inline, no function)
n_eval   = 120
bw       = 1.5
x_eval   = collect(range(minimum(mean_expr), maximum(mean_expr), length=n_eval))
y_smooth = zeros(n_eval)
for i in eachindex(x_eval)
    w       = exp.(-((mean_expr .- x_eval[i]) ./ bw) .^ 2)
    wsum    = sum(w)
    y_smooth[i] = wsum > 1e-10 ? sum(w .* lfc) / wsum : 0.0
end

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

ax = Axis(
    fig[1, 1];
    title              = "ma-differential-expression · julia · makie · anyplot.ai",
    titlesize          = 20,
    titlecolor         = INK,
    xlabel             = "Mean log₂ Expression",
    ylabel             = "log₂ Fold Change",
    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,
    xgridvisible       = false,
    ygridcolor         = RGBAf(INK.r, INK.g, INK.b, 0.10),
    xminorgridvisible  = false,
    yminorgridvisible  = false,
)

# Layer 1: non-significant genes (large cloud, semi-transparent)
scatter!(ax, mean_expr[not_sig], lfc[not_sig];
    color       = (INK_MUTED, 0.18),
    markersize  = 4,
    strokewidth = 0,
)

# Layer 2: significantly up-regulated genes (brand green — gain)
scatter!(ax, mean_expr[sig_up], lfc[sig_up];
    color       = (BRAND_GREEN, 0.65),
    markersize  = 9,
    strokewidth = 0.5,
    strokecolor = INK_SOFT,
)

# Layer 3: significantly down-regulated genes (matte red — loss)
scatter!(ax, mean_expr[sig_down], lfc[sig_down];
    color       = (MATTE_RED, 0.65),
    markersize  = 9,
    strokewidth = 0.5,
    strokecolor = INK_SOFT,
)

# Reference lines: zero line and ±1 log2 FC thresholds
hlines!(ax, [0.0]; color = INK_SOFT, linewidth = 1.5)
hlines!(ax, [1.0, -1.0]; color = INK_SOFT, linewidth = 0.9, linestyle = :dash)

# LOESS-like smoothing curve
lines!(ax, x_eval, y_smooth; color = OCHRE, linewidth = 3.0)

# Legend
Legend(
    fig[1, 2],
    [
        MarkerElement(
            color = RGBAf(INK_MUTED.r, INK_MUTED.g, INK_MUTED.b, 0.55),
            marker = :circle, markersize = 14,
        ),
        MarkerElement(color = BRAND_GREEN, marker = :circle, markersize = 14),
        MarkerElement(color = MATTE_RED,   marker = :circle, markersize = 14),
        LineElement(color = OCHRE, linewidth = 2.5),
    ],
    [
        "Not significant",
        "Up-regulated (adj. p < 0.05)",
        "Down-regulated (adj. p < 0.05)",
        "LOESS trend",
    ],
    framevisible    = true,
    framecolor      = INK_SOFT,
    backgroundcolor = ELEVATED_BG,
    labelcolor      = INK,
    labelsize       = 14,
)

colsize!(fig.layout, 1, Relative(0.78))

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

Part of MA Plot for Differential Expression on anyplot.ai.

Other implementations