Interactive UpSet plots for Plotly Dash, as a drop-in component or a plain figure.
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.
Columns are intersections, rows are sets. Filled connected dots show which sets participate in each intersection.
The cardinality of every intersection, on a common scale, so the big overlaps are obvious at a glance.
The total size of each set, aligned to the matrix rows.
distinct (exclusive), intersect (inclusive), and union counting — the analytic knob most tools omit.
By cardinality, degree, or deviation, with deterministic tie-breaking. Sets by size or name.
Minimum and maximum subset size and degree, top-N intersections, and hide-empty.
How surprising each intersection is versus independence — on hover and as a sort key.
It's a Plotly figure: interactive in the browser, or static PNG/SVG/PDF via kaleido.
Indicator tables from pandas, Polars, PyArrow, and more, through narwhals — no pandas dependency.
conda install -c conda-forge dash-upsetmamba install -c conda-forge dash-upsetpixi add dash-upsetpip install dash-upsetimport 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)