Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- Programmatic highlighting via `highlight` parameter (`"least"`, `"greatest"`, index, or list of indices)

### Changed

- Out-of-bounds highlight indices now raise errors instead of failing silently

## [0.4.0] - 2025-01-20

### Added
Expand Down
97 changes: 94 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,64 @@
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

Raises
------
IndexError
If a single integer index is out of bounds
ValueError
If any index in a list is out of bounds
"""
# 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):
raise IndexError(
f"highlight index {highlight} is out of bounds for "
f"{len(intersections)} intersections"
)
return [intersections.iloc[highlight]["intersection_id"]]
else: # isinstance(highlight, list)
# Validate all indices first
invalid_indices = [i for i in highlight if i >= len(intersections)]
if invalid_indices:
raise ValueError(
f"highlight indices {invalid_indices} are out of bounds for "
f"{len(intersections)} intersections"
)
return [intersections.iloc[i]["intersection_id"] for i in highlight]


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

Expand Down Expand Up @@ -84,6 +142,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 +189,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 +266,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 +290,20 @@ 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