Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 79 additions & 3 deletions altair_upset/upset.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import List, Optional, Union
from typing import List, Literal, Optional, Union

import altair as alt
import pandas as pd
Expand All @@ -9,6 +9,51 @@
from .transforms import create_base_chart


def _determine_highlighted_intersections(
data: pd.DataFrame,
highlight: Union[Literal["least", "greatest"], int, List[int]],
) -> List[float]:
"""Determine which intersection IDs to highlight based on the highlight parameter.

Parameters
----------
data : pd.DataFrame
The preprocessed data with intersection_id and count columns
highlight : "least", "greatest", int, or list of int
The highlighting criteria

Returns
-------
list of float
List of intersection_ids to highlight
"""
# Get unique intersections with their counts
intersections = (
data.groupby("intersection_id")["count"]
.first()
.reset_index()
.sort_index()
)

if isinstance(highlight, str):
if highlight == "least":
min_idx = intersections["count"].idxmin()
return [intersections.loc[min_idx, "intersection_id"]]
else: # highlight == "greatest"
max_idx = intersections["count"].idxmax()
return [intersections.loc[max_idx, "intersection_id"]]
elif isinstance(highlight, int):
if highlight < len(intersections):
return [intersections.iloc[highlight]["intersection_id"]]
return []
else: # isinstance(highlight, list)
return [
intersections.iloc[i]["intersection_id"]
for i in highlight
if i < len(intersections)

Copilot AI Nov 12, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When an integer index is out of bounds, the function silently returns an empty list. This could lead to confusing behavior where no intersection is highlighted without any indication of why. Consider raising an IndexError or ValueError with a descriptive message instead.

Suggested change
if highlight < len(intersections):
return [intersections.iloc[highlight]["intersection_id"]]
return []
else: # isinstance(highlight, list)
return [
intersections.iloc[i]["intersection_id"]
for i in highlight
if i < len(intersections)
if 0 <= highlight < len(intersections):
return [intersections.iloc[highlight]["intersection_id"]]
raise IndexError(
f"Highlight index {highlight} is out of bounds for intersections of length {len(intersections)}."
)
else: # isinstance(highlight, list)
invalid_indices = [i for i in highlight if not (0 <= i < len(intersections))]
if invalid_indices:
raise IndexError(
f"Highlight indices {invalid_indices} are out of bounds for intersections of length {len(intersections)}."
)
return [
intersections.iloc[i]["intersection_id"]
for i in highlight

Copilot uses AI. Check for mistakes.

Copilot AI Nov 12, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When a list contains out-of-bounds indices, they are silently filtered out. This could cause confusion if a user expects all specified intersections to be highlighted. Consider raising an error if any index is out of bounds, or at least logging a warning.

Suggested change
return [
intersections.iloc[i]["intersection_id"]
for i in highlight
if i < len(intersections)
out_of_bounds = [i for i in highlight if i < 0 or i >= len(intersections)]
if out_of_bounds:
raise ValueError(
f"The following indices are out of bounds for intersections (valid range: 0 to {len(intersections)-1}): {out_of_bounds}"
)
return [
intersections.iloc[i]["intersection_id"]
for i in highlight

Copilot uses AI. Check for mistakes.
]


class UpSetChart:
"""A wrapper class for UpSet plots."""

Expand Down Expand Up @@ -84,6 +129,7 @@ def UpSetAltair(
"#BDC6CA",
],
highlight_color: str = "#EA4667",
highlight: Optional[Union[Literal["least", "greatest"], int, List[int]]] = None,
glyph_size: int = 100, # Reduced from 200
set_label_bg_size: int = 500, # Reduced from 1000
line_connection_size: int = 1, # Reduced from 2
Expand Down Expand Up @@ -130,7 +176,14 @@ def UpSetAltair(
color_range : list of str
List of colors for the sets. Defaults to a colorblind-friendly palette.
highlight_color : str, default "#EA4667"
Color used for highlighting on hover.
Color used for highlighting on hover or programmatic highlighting.
highlight : str, int, or list of int, optional
Specifies which intersections to highlight programmatically:
- None (default): interactive hover highlighting
- "least": highlight the intersection with the smallest size
- "greatest": highlight the intersection with the largest size
- int: highlight the intersection at the specified index (0-based)
- list of int: highlight multiple intersections by their indices
glyph_size : int, default 200
Size of the matrix glyphs in pixels.
set_label_bg_size : int, default 1000
Expand Down Expand Up @@ -200,6 +253,18 @@ def UpSetAltair(
raise ValueError("if provided, abbre must have the same length as sets")
if vertical_bar_y_axis_orient not in ["left", "right"]:
raise ValueError("vertical bar y axis orient must be 'left' or 'right'")
if highlight is not None:
if isinstance(highlight, str):
if highlight not in ["least", "greatest"]:
raise ValueError("highlight string must be 'least' or 'greatest'")
elif isinstance(highlight, int):
if highlight < 0:
raise ValueError("highlight index must be non-negative")
elif isinstance(highlight, list):
if not all(isinstance(i, int) and i >= 0 for i in highlight):
raise ValueError("highlight list must contain non-negative integers")
else:
raise TypeError("highlight must be None, str, int, or list of int")

# Apply theme if specified
if theme is not None:
Expand All @@ -212,7 +277,18 @@ def UpSetAltair(

# Setup selections for interactivity
legend_selection = alt.selection_point(fields=["set"], bind="legend")
color_selection = alt.selection_point(fields=["intersection_id"], on="mouseover")

# Setup color selection based on highlight parameter
if highlight is None:
# Default hover behavior
color_selection = alt.selection_point(fields=["intersection_id"], on="mouseover")
else:
# Determine which intersections to highlight and create fixed selection
highlighted_ids = _determine_highlighted_intersections(data, highlight)
color_selection = alt.selection_point(
fields=["intersection_id"],
value=[{"intersection_id": id_} for id_ in highlighted_ids]
)

# Calculate dimensions
if horizontal_bar_chart_width is None:
Expand Down
72 changes: 72 additions & 0 deletions docs/examples/advanced_features.rst
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,75 @@ Let's analyze the engagement patterns across different platform combinations:
print(f"Avg Time: {row['avg_time']:.1f} minutes")
print(f"Avg Posts: {row['avg_posts']:.1f} per week")
print(f"Avg Engagement: {row['avg_engagement']:.1f}%")

Programmatic Highlighting
-------------------------

You can programmatically highlight specific intersections using the ``highlight`` parameter.
This is useful for drawing attention to specific patterns without requiring user interaction.

Highlighting the Largest Intersection
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Highlight the intersection with the most users:

.. altair-plot::

au.UpSetAltair(
data=data,
sets=platforms,
title="Social Media Platform Usage - Largest Intersection Highlighted",
highlight="greatest",
width=800,
height=500
).chart

Highlighting the Smallest Intersection
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Highlight the intersection with the fewest users:

.. altair-plot::

au.UpSetAltair(
data=data,
sets=platforms,
title="Social Media Platform Usage - Smallest Intersection Highlighted",
highlight="least",
width=800,
height=500
).chart

Highlighting Specific Intersections by Index
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Highlight specific intersections by their index (0-based):

.. altair-plot::

# Highlight the first intersection
au.UpSetAltair(
data=data,
sets=platforms,
title="Social Media Platform Usage - First Intersection Highlighted",
highlight=0,
width=800,
height=500
).chart

Highlighting Multiple Intersections
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Highlight multiple intersections at once:

.. altair-plot::

# Highlight the first three intersections
au.UpSetAltair(
data=data,
sets=platforms,
title="Social Media Platform Usage - Multiple Intersections Highlighted",
highlight=[0, 1, 2],
width=800,
height=500
).chart
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading