dash-upset

Interactive UpSet plots for Plotly Dash, as a drop-in component or a plain figure.

License MIT Python 3.10+ Built on Plotly
Try it live View on GitHub

What is an UpSet plot?

An UpSet plot visualizes the intersections of many sets. Venn and Euler diagrams become unreadable past three or four sets; UpSet replaces the overlapping circles with a compact, sortable matrix plus aligned bar charts.

The matrix

Columns are intersections, rows are sets. Filled connected dots show which sets participate in each intersection.

Intersection-size bars

The cardinality of every intersection, on a common scale, so the big overlaps are obvious at a glance.

Set-size bars

The total size of each set, aligned to the matrix rows.

Install

conda install -c conda-forge dash-upset
mamba install -c conda-forge dash-upset
pixi add dash-upset
pip install dash-upset

Quick start

import pandas as pd
from dash import Dash, Input, Output, callback, html
from dash_upset import UpSet

# One row per misclassified test example; 1 = that model got it wrong.
df = pd.DataFrame(
    {
        "ResNet": [1, 1, 0, 1, 0, 1],
        "ViT": [1, 1, 1, 0, 0, 1],
        "XGBoost": [0, 1, 1, 1, 1, 0],
    }
)

app = Dash(__name__)
app.layout = html.Div(
    [
        UpSet(id="errors", data=df, sets=["ResNet", "ViT", "XGBoost"]),
        html.Pre(id="out"),
    ]
)


@callback(Output("out", "children"), Input("errors", "selected_intersection"))
def show(selection):
    # {"label": "ResNet & ViT", "sets": ["ResNet", "ViT"], "size": 2}
    return str(selection)


if __name__ == "__main__":
    app.run(debug=True)
from dash_upset import create_upset

fig = create_upset(df, sets=["ResNet", "ViT", "XGBoost"], title="Model error sets")
fig.show()  # or dcc.Graph(figure=fig); export to PNG/SVG/PDF via kaleido

Pre-aggregated intersection sizes, keyed by &-joined set names. Best when you already have counts.

from dash import Dash
from dash_upset import UpSet, from_counts

data = from_counts(
    {
        "ResNet": 140,
        "ViT": 120,
        "ResNet&ViT": 210,  # "&" joins set names; override with sep=
    },
    sep="&",
)

app = Dash(__name__)
app.layout = UpSet(id="errors", data=data)

One entry per element: the tuple of sets it belongs to. Best when you have raw per-record labels.

from dash import Dash
from dash_upset import UpSet, from_memberships

data = from_memberships(
    [
        ("ResNet", "ViT"),
        ("ResNet",),
        ("ViT", "XGBoost"),
        (),
    ]
)

app = Dash(__name__)
app.layout = UpSet(id="errors", data=data)

Per-set element ids; the overlaps are computed for you. Best when you have a membership list per set.

from dash import Dash
from dash_upset import UpSet, from_contents

data = from_contents(
    {
        "ResNet": ["img1", "img2", "img3"],
        "ViT": ["img2", "img3", "img4"],
        "XGBoost": ["img3", "img5"],
    }
)

app = Dash(__name__)
app.layout = UpSet(id="errors", data=data)

Wire the selection into the rest of your dashboard. A click lists the members of the clicked intersection — the drill-into-members pattern, straight from the model's stored element ids.

from dash import Dash, Input, Output, callback, html
from dash_upset import UpSet, from_contents

# Element-level data keeps each intersection's member ids.
contents = {
    "ResNet": ["img1", "img2", "img5", "img7"],
    "ViT": ["img2", "img5", "img8"],
    "XGBoost": ["img2", "img3", "img5"],
}
data = from_contents(contents)
members = {frozenset(i.sets): i.elements for i in data.intersections}

app = Dash(__name__)
app.layout = html.Div(
    [
        UpSet(id="errors", data=data),
        html.Pre(id="members"),
    ]
)


@callback(Output("members", "children"), Input("errors", "selected_intersection"))
def show_members(selection):
    if not selection:
        return "Click an intersection to list its members."
    ids = members.get(frozenset(selection["sets"]), ())
    return f"{selection['label']}: {', '.join(map(str, ids))}"


if __name__ == "__main__":
    app.run(debug=True)