Skip to content

Commit fe5e3f5

Browse files
committed
Add viz/temporal extras and make plotting code import-safe without matplotlib
1 parent 039d5ee commit fe5e3f5

File tree

9 files changed

+76
-18
lines changed

9 files changed

+76
-18
lines changed

.github/workflows/tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ jobs:
4343
- name: Install
4444
run: |
4545
python -m pip install -e ".[dev]"
46-
python -m pip install matplotlib
46+
python -m pip install matplotlib tqdm
4747
4848
- name: Pytest
4949
run: pytest

hypergraphx/viz/draw_communities.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from typing import Optional, Tuple, Union
22

3-
import matplotlib.pyplot as plt
43
import networkx as nx
54
import numpy as np
65

@@ -13,7 +12,7 @@ def draw_communities(
1312
u: np.array,
1413
col: dict,
1514
figsize: tuple = (7, 7),
16-
ax: Optional[plt.Axes] = None,
15+
ax: Optional["plt.Axes"] = None,
1716
pos: Optional[dict] = None,
1817
edge_color: str = "lightgrey",
1918
edge_width: float = 0.3,
@@ -33,6 +32,13 @@ def draw_communities(
3332
opt_dist: float = 0.2,
3433
show: bool = False,
3534
):
35+
try:
36+
import matplotlib.pyplot as plt # type: ignore
37+
except ImportError as exc: # pragma: no cover
38+
raise ImportError(
39+
"draw_communities requires matplotlib. Install with `pip install hypergraphx[viz]`."
40+
) from exc
41+
3642
"""Visualize the node memberships of a hypergraph. Nodes are colored according to their memberships,
3743
which can be either hard- or soft-membership, and the node size is proportional to the degree in the hypergraph.
3844
Edges are the pairwise interactions of the hypergraph clique projection.

hypergraphx/viz/draw_hypergraph.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import random
22
from typing import Optional, Union
33

4-
import matplotlib.pyplot as plt
54
import networkx as nx
65
import numpy as np
76

@@ -74,7 +73,7 @@ def Smooth_by_Chaikin(self, number_of_refinements):
7473
def draw_hypergraph(
7574
hypergraph: Hypergraph,
7675
figsize: tuple = (12, 7),
77-
ax: Optional[plt.Axes] = None,
76+
ax: Optional["plt.Axes"] = None,
7877
pos: Optional[dict] = None,
7978
edge_color: str = "lightgrey",
8079
hyperedge_color_by_order: Optional[dict] = None,
@@ -106,6 +105,12 @@ def draw_hypergraph(
106105
matplotlib.axes.Axes
107106
The axes the plot was drawn on.
108107
"""
108+
try:
109+
import matplotlib.pyplot as plt # type: ignore
110+
except ImportError as exc: # pragma: no cover
111+
raise ImportError(
112+
"draw_hypergraph requires matplotlib. Install with `pip install hypergraphx[viz]`."
113+
) from exc
109114

110115
def _stable_color(order_value):
111116
rng = random.Random(order_value)

hypergraphx/viz/draw_motifs.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
1-
import matplotlib.pyplot as plt
21
import networkx as nx
32
import itertools
4-
from matplotlib.patches import Polygon
53

64

75
def draw_motifs(
@@ -37,6 +35,14 @@ def draw_motifs(
3735
fig : matplotlib.figure.Figure
3836
axes : list[matplotlib.axes.Axes]
3937
"""
38+
try:
39+
import matplotlib.pyplot as plt # type: ignore
40+
from matplotlib.patches import Polygon # type: ignore
41+
except ImportError as exc: # pragma: no cover
42+
raise ImportError(
43+
"draw_motifs requires matplotlib. Install with `pip install hypergraphx[viz]`."
44+
) from exc
45+
4046
# Collect all unique nodes across all patterns
4147
all_nodes = set(
4248
itertools.chain.from_iterable(itertools.chain.from_iterable(patterns))

hypergraphx/viz/draw_projections.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import matplotlib.pyplot as plt
21
import networkx as nx
32

43
from hypergraphx import Hypergraph
@@ -33,6 +32,13 @@ def draw_bipartite(
3332
ax : matplotlib.axes.Axes.
3433
The axes the graph was drawn on.
3534
"""
35+
try:
36+
import matplotlib.pyplot as plt # type: ignore
37+
except ImportError as exc: # pragma: no cover
38+
raise ImportError(
39+
"draw_bipartite requires matplotlib. Install with `pip install hypergraphx[viz]`."
40+
) from exc
41+
3642
g, _ = bipartite_projection(h)
3743

3844
if pos is None:
@@ -77,6 +83,13 @@ def draw_clique(h: Hypergraph, pos=None, ax=None, show: bool = False, **kwargs):
7783
-------
7884
ax : matplotlib.axes.Axes. The axes the graph was drawn on.
7985
"""
86+
try:
87+
import matplotlib.pyplot as plt # type: ignore
88+
except ImportError as exc: # pragma: no cover
89+
raise ImportError(
90+
"draw_clique requires matplotlib. Install with `pip install hypergraphx[viz]`."
91+
) from exc
92+
8093
g = clique_projection(h)
8194

8295
if pos is None:

hypergraphx/viz/draw_simplicial.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import matplotlib.pyplot as plt
21
import networkx as nx
32

43
from hypergraphx.representations.projections import clique_projection
@@ -37,6 +36,13 @@ def draw_simplicial(
3736
matplotlib.axes.Axes
3837
The axes the plot was drawn on.
3938
"""
39+
try:
40+
import matplotlib.pyplot as plt # type: ignore
41+
except ImportError as exc: # pragma: no cover
42+
raise ImportError(
43+
"draw_simplicial requires matplotlib. Install with `pip install hypergraphx[viz]`."
44+
) from exc
45+
4046
G = clique_projection(HG, keep_isolated=True)
4147
if pos is None:
4248
pos = nx.spring_layout(G)

hypergraphx/viz/plot_motifs.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
import math
22

3-
import matplotlib.pyplot as plt
4-
from matplotlib.patches import Polygon
5-
63
from hypergraphx.motifs.utils import generate_motifs
74

85

@@ -105,6 +102,14 @@ def _draw_motif_icon(
105102
):
106103
if not events:
107104
return
105+
# Lazy import of Polygon to keep this module import-safe without matplotlib.
106+
try:
107+
from matplotlib.patches import Polygon # type: ignore
108+
except ImportError as exc: # pragma: no cover
109+
raise ImportError(
110+
"plot_motifs requires matplotlib. Install with `pip install hypergraphx[viz]`."
111+
) from exc
112+
108113
node_ids = sorted({n for ev in events for n in ev})
109114
k = len(node_ids)
110115
if k == 0:
@@ -261,6 +266,14 @@ def plot_motifs(
261266
annotate_fmt: str = "{:+.2f}",
262267
hover_labels: bool = False,
263268
):
269+
try:
270+
import matplotlib.pyplot as plt # type: ignore
271+
from matplotlib.patches import Polygon # type: ignore
272+
except ImportError as exc: # pragma: no cover
273+
raise ImportError(
274+
"plot_motifs requires matplotlib. Install with `pip install hypergraphx[viz]`."
275+
) from exc
276+
264277
"""
265278
Plot motifs. Motifs are sorted in such a way to show first lower order motifs, then higher order motifs.
266279

pyproject.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,21 @@ dev = [
3737
"black>=24.3.0",
3838
"pre-commit",
3939
]
40+
temporal = [
41+
"tqdm",
42+
]
43+
viz = [
44+
"matplotlib",
45+
# optional interactive hover labels in plot_motifs
46+
"mplcursors",
47+
]
4048
docs = [
4149
"sphinx",
4250
"furo",
4351
"nbsphinx",
4452
"matplotlib",
4553
"seaborn",
54+
"tqdm",
4655
]
4756
ml = [
4857
"scikit-learn",

tests/measures/test_shortest_paths_helpers.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,13 @@ def test_calc_sizes_redundancies_and_props():
5959
assert "avg_size" in sample
6060
assert len(sample["sizes"]) == len(sample["sp"]) - 1
6161

62-
avg_ord = spl.copy()
63-
avg_ord.values[:] = 2
64-
np.fill_diagonal(avg_ord.values, 0)
65-
avg_ord.values[0, 1] = 3
66-
avg_ord.values[1, 0] = 3
62+
# Avoid mutating DataFrame .values directly (can be read-only under some pandas modes).
63+
avg_ord = np.full_like(spl.to_numpy(), 2.0)
64+
np.fill_diagonal(avg_ord, 0)
65+
avg_ord[0, 1] = 3
66+
avg_ord[1, 0] = 3
6767
# If only one boolean class appears, unstack drops the other column.
68-
props = calc_prop_true_dyad_paths_per_spl(spl.to_numpy(), avg_ord.to_numpy())
68+
props = calc_prop_true_dyad_paths_per_spl(spl.to_numpy(), avg_ord)
6969
assert "spl" in props.columns
7070
assert any(col in props.columns for col in ("prop_False", "prop_True"))
7171

0 commit comments

Comments
 (0)