UpSet Plot for Multi-Set Intersection Analysis — Bokeh

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 Bokeh

Renders

Python source (Bokeh)

""" anyplot.ai
upset-basic: UpSet Plot for Multi-Set Intersection Analysis
Library: bokeh 3.9.0 | Python 3.13.13
Quality: 86/100 | Created: 2026-05-13
"""

import os
import time
from pathlib import Path

import numpy as np
from bokeh.io import output_file, save
from bokeh.layouts import gridplot
from bokeh.models import FixedTicker, Range1d
from bokeh.plotting import figure
from selenium import webdriver
from selenium.webdriver.chrome.options import Options


# Theme tokens
THEME = os.getenv("ANYPLOT_THEME", "light")
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F"
BRAND = "#009E73"

# Data: genes from 6 genomic differential expression experiments
np.random.seed(42)
n_genes = 800
set_names = ["Exp A", "Exp B", "Exp C", "Exp D", "Exp E", "Exp F"]
n_sets = len(set_names)

membership_probs = [0.38, 0.33, 0.27, 0.30, 0.24, 0.21]
membership = np.zeros((n_genes, n_sets), dtype=bool)
for j, prob in enumerate(membership_probs):
    membership[:, j] = np.random.rand(n_genes) < prob
for i in range(n_genes):
    if not membership[i].any():
        membership[i, np.random.randint(n_sets)] = True

set_sizes = membership.sum(axis=0)

# Compute intersection signatures and counts
sig_counts = {}
for i in range(n_genes):
    sig = frozenset(int(j) for j in np.where(membership[i])[0])
    sig_counts[sig] = sig_counts.get(sig, 0) + 1

intersections = sorted(sig_counts.items(), key=lambda x: -x[1])[:12]
n_inter = len(intersections)
inter_counts = [count for _, count in intersections]
max_count = max(inter_counts)

# y positions: Exp A at top (y=n_sets-1), Exp F at bottom (y=0)
set_y = {name: n_sets - 1 - i for i, name in enumerate(set_names)}

# Shared axis ranges for panel alignment
x_range = Range1d(-0.5, n_inter - 0.5)
y_range = Range1d(-0.5, n_sets - 0.5)

W_SETS, W_MATRIX = 1100, 3700
H_BAR, H_MATRIX = 900, 1500

# === DOT MATRIX (bottom right) ===
p_matrix = figure(width=W_MATRIX, height=H_MATRIX, x_range=x_range, y_range=y_range, toolbar_location=None)
p_matrix.background_fill_color = PAGE_BG
p_matrix.border_fill_color = PAGE_BG
p_matrix.outline_line_color = None

# Inactive (hollow) dots
inactive_x, inactive_y = [], []
for col_idx, (sig, _) in enumerate(intersections):
    for set_idx in range(n_sets):
        if set_idx not in sig:
            inactive_x.append(col_idx)
            inactive_y.append(set_y[set_names[set_idx]])

p_matrix.scatter(
    x=inactive_x,
    y=inactive_y,
    size=26,
    marker="circle",
    fill_alpha=0,
    line_color=INK_MUTED,
    line_alpha=0.40,
    line_width=2,
)

# Connecting lines between active sets in each column
seg_x0, seg_x1, seg_y0, seg_y1 = [], [], [], []
for col_idx, (sig, _) in enumerate(intersections):
    if len(sig) > 1:
        ys = sorted(set_y[set_names[j]] for j in sig)
        seg_x0.append(col_idx)
        seg_x1.append(col_idx)
        seg_y0.append(ys[0])
        seg_y1.append(ys[-1])

p_matrix.segment(x0=seg_x0, x1=seg_x1, y0=seg_y0, y1=seg_y1, line_color=BRAND, line_width=14, alpha=0.75)

# Active (filled) dots on top
active_x, active_y = [], []
for col_idx, (sig, _) in enumerate(intersections):
    for set_idx in sig:
        active_x.append(col_idx)
        active_y.append(set_y[set_names[set_idx]])

p_matrix.scatter(
    x=active_x, y=active_y, size=34, marker="circle", fill_color=BRAND, line_color=PAGE_BG, line_width=2, alpha=0.95
)

y_ticks = list(range(n_sets))
p_matrix.yaxis.ticker = FixedTicker(ticks=y_ticks)
p_matrix.yaxis.major_label_overrides = {set_y[name]: name for name in set_names}
p_matrix.yaxis.major_label_text_font_size = "20pt"
p_matrix.yaxis.major_label_text_color = INK_SOFT
p_matrix.yaxis.axis_line_color = INK_SOFT
p_matrix.yaxis.major_tick_line_color = None
p_matrix.yaxis.minor_tick_line_color = None

p_matrix.xaxis.ticker = FixedTicker(ticks=list(range(n_inter)))
p_matrix.xaxis.major_label_text_font_size = "0pt"
p_matrix.xaxis.axis_line_color = INK_SOFT
p_matrix.xaxis.major_tick_line_color = None
p_matrix.xaxis.minor_tick_line_color = None

p_matrix.xgrid.grid_line_color = None
p_matrix.ygrid.ticker = FixedTicker(ticks=y_ticks)
p_matrix.ygrid.grid_line_color = INK
p_matrix.ygrid.grid_line_alpha = 0.08


# === INTERSECTION BAR CHART (top right) ===
p_bar = figure(
    width=W_MATRIX,
    height=H_BAR,
    x_range=x_range,
    y_range=Range1d(0, max_count * 1.18),
    toolbar_location=None,
    title="upset-basic · bokeh · anyplot.ai",
)
p_bar.background_fill_color = PAGE_BG
p_bar.border_fill_color = PAGE_BG
p_bar.outline_line_color = None

p_bar.vbar(
    x=list(range(n_inter)), top=inter_counts, width=0.55, color=BRAND, alpha=0.85, line_color=PAGE_BG, line_width=1.5
)

for xi, h in enumerate(inter_counts):
    p_bar.text(
        x=[xi],
        y=[h + max_count * 0.03],
        text=[str(h)],
        text_align="center",
        text_baseline="bottom",
        text_font_size="17pt",
        text_color=INK_SOFT,
    )

p_bar.title.text_font_size = "28pt"
p_bar.title.text_color = INK
p_bar.title.align = "center"

p_bar.yaxis.axis_label = "Intersection Size"
p_bar.yaxis.axis_label_text_font_size = "20pt"
p_bar.yaxis.axis_label_text_color = INK
p_bar.yaxis.major_label_text_font_size = "16pt"
p_bar.yaxis.major_label_text_color = INK_SOFT
p_bar.yaxis.axis_line_color = INK_SOFT
p_bar.yaxis.major_tick_line_color = INK_SOFT
p_bar.yaxis.minor_tick_line_color = None

p_bar.xaxis.ticker = FixedTicker(ticks=list(range(n_inter)))
p_bar.xaxis.major_label_text_font_size = "0pt"
p_bar.xaxis.axis_line_color = None
p_bar.xaxis.major_tick_line_color = None
p_bar.xaxis.minor_tick_line_color = None

p_bar.xgrid.grid_line_color = None
p_bar.ygrid.grid_line_color = INK
p_bar.ygrid.grid_line_alpha = 0.10


# === SET SIZE BAR CHART (bottom left) ===
max_set_size = int(set_sizes.max())
p_sets = figure(
    width=W_SETS, height=H_MATRIX, x_range=Range1d(max_set_size * 1.20, 0), y_range=y_range, toolbar_location=None
)
p_sets.background_fill_color = PAGE_BG
p_sets.border_fill_color = PAGE_BG
p_sets.outline_line_color = None

p_sets.hbar(
    y=[set_y[name] for name in set_names],
    right=list(set_sizes),
    height=0.5,
    color=BRAND,
    alpha=0.80,
    line_color=PAGE_BG,
    line_width=1.5,
)

p_sets.xaxis.axis_label = "Set Size"
p_sets.xaxis.axis_label_text_font_size = "18pt"
p_sets.xaxis.axis_label_text_color = INK
p_sets.xaxis.major_label_text_font_size = "16pt"
p_sets.xaxis.major_label_text_color = INK_SOFT
p_sets.xaxis.axis_line_color = INK_SOFT
p_sets.xaxis.major_tick_line_color = INK_SOFT
p_sets.xaxis.minor_tick_line_color = None

p_sets.yaxis.visible = False
p_sets.xgrid.grid_line_color = None
p_sets.ygrid.grid_line_color = None


# === CORNER SPACER (top left) ===
p_corner = figure(width=W_SETS, height=H_BAR, x_range=Range1d(0, 1), y_range=Range1d(0, 1), toolbar_location=None)
p_corner.background_fill_color = PAGE_BG
p_corner.border_fill_color = PAGE_BG
p_corner.outline_line_color = None
p_corner.xaxis.visible = False
p_corner.yaxis.visible = False
p_corner.xgrid.grid_line_color = None
p_corner.ygrid.grid_line_color = None
p_corner.rect(x=[0.5], y=[0.5], width=[0], height=[0], fill_alpha=0, line_alpha=0)


# === Grid Layout ===
grid = gridplot([[p_corner, p_bar], [p_sets, p_matrix]], merge_tools=False, toolbar_location=None)

output_file(f"plot-{THEME}.html")
save(grid)

# Screenshot with headless Chrome via Selenium
W, H = 4800, 2700
opts = Options()
for arg in (
    "--headless=new",
    "--no-sandbox",
    "--disable-dev-shm-usage",
    "--disable-gpu",
    f"--window-size={W},{H}",
    "--hide-scrollbars",
):
    opts.add_argument(arg)
driver = webdriver.Chrome(options=opts)
driver.set_window_size(W, H)
driver.get(f"file://{Path(f'plot-{THEME}.html').resolve()}")
time.sleep(3)
driver.save_screenshot(f"plot-{THEME}.png")
driver.quit()

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

Other implementations