UpSet Plot for Multi-Set Intersection Analysis — Makie.jl

An UpSet plot visualizes intersections of multiple sets using a matrix-based layout that scales far better than Venn diagrams beyond 3 sets. A horizontal bar chart shows individual set sizes, a dot-matrix indicates which sets participate in each intersection, and a vertical bar chart above shows the intersection cardinality. This is the modern standard for set intersection analysis, making complex overlaps between many sets immediately readable.

UpSet Plot for Multi-Set Intersection Analysis rendered with Makie.jl

Renders

Julia source (Makie.jl)

# anyplot.ai
# upset-basic: UpSet Plot for Multi-Set Intersection Analysis
# Library: makie 0.21.9 | Julia 1.11.9
# Quality: 96/100 | Created: 2026-09-09

using CairoMakie
using Colors
using Random

Random.seed!(42)

# --- Theme tokens (see prompts/default-style-guide.md "Theme-adaptive Chrome") ---
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 INK_MUTED  = THEME == "light" ? colorant"#6B6A63" : colorant"#A8A79F"
const GRID_RGBA  = RGBAf(INK.r, INK.g, INK.b, 0.15)

# Imprint palette (see prompts/default-style-guide.md "Categorical Palette")
const BRAND      = colorant"#009E73"  # position 1 — ALWAYS first series
const IMPRINT_SEQ = cgrad([colorant"#009E73", colorant"#4467A3"])  # sequential — degree encoding

# --- Data: differential-expression gene sets across sequencing experiments ---
# Most genes are assay-specific hits (drawn independently per set), but a
# "pan-omics" subset of broadly-active genes shows up across most assays —
# a realistic biological pattern that also produces genuine degree-4/5
# intersections rather than the top-15 being dominated by degree <= 3.
set_names = ["RNA-seq", "ChIP-seq", "ATAC-seq", "Proteomics", "Methylation"]
n_sets = length(set_names)
n_genes = 3000
membership_probs = [0.34, 0.27, 0.21, 0.16, 0.11]
hub_frac = 0.10   # fraction of genes that are broadly-active "pan-omics" hits
hub_prob = 0.75   # per-set membership probability for those hub genes

membership = falses(n_genes, n_sets)
for i in 1:n_genes
    is_hub = rand() < hub_frac
    for j in 1:n_sets
        p = is_hub ? hub_prob : membership_probs[j]
        membership[i, j] = rand() < p
    end
end
keep = [any(view(membership, i, :)) for i in 1:n_genes]
membership = membership[keep, :]
n_elements = size(membership, 1)

# Sort sets by total size (largest drawn at the top of the matrix)
set_sizes = vec(sum(membership, dims = 1))
set_order = sortperm(set_sizes, rev = true)
set_names_sorted = set_names[set_order]
set_sizes_sorted = set_sizes[set_order]
membership = membership[:, set_order]
set_y = [n_sets - k + 1 for k in 1:n_sets]  # top row = largest set

# Count occurrences of each membership pattern, keep the top 15 by size
combo_counts = Dict{Vector{Bool}, Int}()
for i in 1:n_elements
    key = membership[i, :]
    combo_counts[key] = get(combo_counts, key, 0) + 1
end
combos = collect(keys(combo_counts))
counts = [combo_counts[c] for c in combos]
combo_order = sortperm(counts, rev = true)
n_show = min(15, length(combos))
top_combos = combos[combo_order[1:n_show]]
top_counts = counts[combo_order[1:n_show]]
degrees = [sum(c) for c in top_combos]
deg_min, deg_max = extrema(degrees)
deg_span = max(deg_max - deg_min, 1)
bar_colors = [get(IMPRINT_SEQ, (d - deg_min) / deg_span) for d in degrees]

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

Label(
    fig[0, 1:3],
    "upset-basic · julia · makie · anyplot.ai",
    fontsize = 22,
    font = :bold,
    color = INK,
)

ax_bars = Axis(
    fig[1, 2];
    ylabel = "Intersection size",
    ylabelcolor = INK,
    ylabelsize = 14,
    yticklabelcolor = INK_SOFT,
    yticklabelsize = 12,
    xticksvisible = false,
    xticklabelsvisible = false,
    backgroundcolor = PAGE_BG,
    topspinevisible = false,
    rightspinevisible = false,
    bottomspinevisible = false,
    leftspinecolor = INK_SOFT,
    ygridcolor = GRID_RGBA,
    ygridvisible = true,
    xgridvisible = false,
)

ax_setbars = Axis(
    fig[2, 1];
    xlabel = "Set size",
    xlabelcolor = INK,
    xlabelsize = 14,
    xticklabelcolor = INK_SOFT,
    xticklabelsize = 12,
    yticksvisible = false,
    yticklabelsvisible = false,
    xreversed = true,
    backgroundcolor = PAGE_BG,
    topspinevisible = false,
    rightspinevisible = false,
    leftspinevisible = false,
    bottomspinecolor = INK_SOFT,
    xgridcolor = GRID_RGBA,
    xgridvisible = true,
    ygridvisible = false,
)

ax_matrix = Axis(
    fig[2, 2];
    yticks = (1:n_sets, reverse(set_names_sorted)),
    yticklabelcolor = INK_SOFT,
    yticklabelsize = 12,
    xticksvisible = false,
    xticklabelsvisible = false,
    backgroundcolor = PAGE_BG,
    topspinevisible = false,
    rightspinevisible = false,
    leftspinevisible = false,
    bottomspinevisible = false,
    ygridvisible = false,
    xgridvisible = false,
)

linkxaxes!(ax_bars, ax_matrix)
linkyaxes!(ax_setbars, ax_matrix)
xlims!(ax_matrix, 0.3, n_show + 0.7)
ylims!(ax_matrix, 0.3, n_sets + 0.7)

# Intersection size bars — colored by degree (how many sets overlap)
barplot!(ax_bars, 1:n_show, top_counts; color = bar_colors, width = 0.65)
ylims!(ax_bars, 0, maximum(top_counts) * 1.15)

# Callout on the single largest intersection to sharpen the "aha"
text!(
    ax_bars, 1, top_counts[1];
    text = string(top_counts[1]),
    align = (:center, :bottom),
    offset = (0, 4),
    fontsize = 13,
    color = INK,
    font = :bold,
)

# Set size bars — single series, brand color
barplot!(ax_setbars, set_y, set_sizes_sorted; direction = :x, color = BRAND, width = 0.65)

# Alternating row bands for readability
for k in 1:n_sets
    if isodd(k)
        hspan!(ax_matrix, set_y[k] - 0.5, set_y[k] + 0.5; color = (INK, 0.04))
    end
end

# Dot matrix — connecting lines first, then non-member and member dots
for j in 1:n_show
    combo = top_combos[j]
    member_rows = [set_y[k] for k in 1:n_sets if combo[k]]
    if length(member_rows) >= 2
        lines!(ax_matrix, fill(j, 2), [minimum(member_rows), maximum(member_rows)];
               color = INK_SOFT, linewidth = 3)
    end
end

member_x = Float64[]; member_yv = Float64[]
absent_x = Float64[]; absent_yv = Float64[]
for j in 1:n_show, k in 1:n_sets
    combo = top_combos[j]
    if combo[k]
        push!(member_x, j); push!(member_yv, set_y[k])
    else
        push!(absent_x, j); push!(absent_yv, set_y[k])
    end
end

scatter!(ax_matrix, absent_x, absent_yv; color = (INK_MUTED, 0.3), markersize = 16)
scatter!(ax_matrix, member_x, member_yv; color = INK, markersize = 24)

Colorbar(
    fig[1, 3];
    limits = (deg_min, deg_max),
    colormap = IMPRINT_SEQ,
    label = "Sets in intersection",
    labelcolor = INK,
    labelsize = 14,
    ticklabelcolor = INK_SOFT,
    ticklabelsize = 12,
    ticks = deg_min:deg_max,
    width = 18,
)

colsize!(fig.layout, 1, Relative(0.14))
colsize!(fig.layout, 2, Relative(0.80))
colsize!(fig.layout, 3, Relative(0.06))
rowsize!(fig.layout, 1, Relative(0.32))
rowsize!(fig.layout, 2, Relative(0.68))

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

Part of UpSet Plot for Multi-Set Intersection Analysis on anyplot.ai.

Other implementations