import random
from typing import Optional, Union
import networkx as nx
import numpy as np
from hypergraphx import Hypergraph
from hypergraphx.representations.projections import clique_projection
[docs]
def sum_points(point1, point2):
x1, y1 = point1
x2, y2 = point2
return x1 + x2, y1 + y2
[docs]
def multiply_point(multiplier, point):
x, y = point
return float(x) * float(multiplier), float(y) * float(multiplier)
[docs]
def is_polygon(cartesian_coords_list):
if (
cartesian_coords_list[0]
== cartesian_coords_list[len(cartesian_coords_list) - 1]
):
return True
else:
return False
[docs]
class Object:
def __init__(self, cartesian_coords_list):
self.Cartesian_coords_list = cartesian_coords_list
[docs]
def Find_Q_point_position(self, P1, P2):
Summand1 = multiply_point(float(3) / float(4), P1)
Summand2 = multiply_point(float(1) / float(4), P2)
Q = sum_points(Summand1, Summand2)
return Q
[docs]
def Find_R_point_position(self, P1, P2):
Summand1 = multiply_point(float(1) / float(4), P1)
Summand2 = multiply_point(float(3) / float(4), P2)
R = sum_points(Summand1, Summand2)
return R
[docs]
def Smooth_by_Chaikin(self, number_of_refinements):
refinement = 1
copy_first_coord = is_polygon(self.Cartesian_coords_list)
obj = Object(self.Cartesian_coords_list)
while refinement <= number_of_refinements:
self.New_cartesian_coords_list = []
for num, tuple in enumerate(self.Cartesian_coords_list):
if num + 1 == len(self.Cartesian_coords_list):
pass
else:
P1, P2 = (tuple, self.Cartesian_coords_list[num + 1])
Q = obj.Find_Q_point_position(P1, P2)
R = obj.Find_R_point_position(P1, P2)
self.New_cartesian_coords_list.append(Q)
self.New_cartesian_coords_list.append(R)
if copy_first_coord:
self.New_cartesian_coords_list.append(self.New_cartesian_coords_list[0])
self.Cartesian_coords_list = self.New_cartesian_coords_list
refinement += 1
return self.Cartesian_coords_list
[docs]
def draw_hypergraph(
hypergraph: Hypergraph,
figsize: tuple = (12, 7),
ax: Optional["plt.Axes"] = None,
pos: Optional[dict] = None,
edge_color: str = "lightgrey",
hyperedge_color_by_order: Optional[dict] = None,
hyperedge_facecolor_by_order: Optional[dict] = None,
edge_width: float = 1.2,
hyperedge_alpha: Union[float, np.array] = 0.8,
node_size: Union[int, np.array] = 150,
node_color: Union[str, np.array] = "#E2E0DD",
node_facecolor: Union[str, np.array] = "black",
node_shape: str = "o",
with_node_labels: bool = False,
label_size: float = 10,
label_col: str = "black",
seed: int = 10,
scale: int = 1,
iterations: int = 100,
opt_dist: float = 0.5,
show: bool = False,
):
"""Visualize a hypergraph.
Parameters
----------
show : bool
If True, call plt.show().
Returns
-------
matplotlib.axes.Axes
The axes the plot was drawn on.
"""
try:
import matplotlib.pyplot as plt # type: ignore
except ImportError as exc: # pragma: no cover
raise ImportError(
"draw_hypergraph requires matplotlib. Install with `pip install hypergraphx[viz]`."
) from exc
def _stable_color(order_value):
rng = random.Random(order_value)
return f"#{rng.randrange(0x1000000):06x}"
# Initialize figure.
if ax is None:
plt.figure(figsize=figsize)
plt.subplot(1, 1, 1)
ax = plt.gca()
# Extract node positions based on the hypergraph clique projection.
if pos is None:
pos = nx.spring_layout(
clique_projection(hypergraph, keep_isolated=True),
iterations=iterations,
seed=seed,
scale=scale,
k=opt_dist,
)
else:
missing_nodes = set(hypergraph.get_nodes()) - set(pos.keys())
if missing_nodes:
raise ValueError("pos is missing positions for some nodes.")
# Set color hyperedges of size > 2 (order > 1).
if hyperedge_color_by_order is None:
hyperedge_color_by_order = {2: "#FFBC79", 3: "#79BCFF", 4: "#4C9F4C"}
else:
hyperedge_color_by_order = dict(hyperedge_color_by_order)
if hyperedge_facecolor_by_order is None:
hyperedge_facecolor_by_order = {2: "#FFBC79", 3: "#79BCFF", 4: "#4C9F4C"}
else:
hyperedge_facecolor_by_order = dict(hyperedge_facecolor_by_order)
# Extract edges (hyperedges of size=2/order=1).
edges = hypergraph.get_edges(order=1)
# Initialize empty graph with the nodes and the pairwise interactions of the hypergraph.
G = nx.Graph()
G.add_nodes_from(hypergraph.get_nodes())
for e in edges:
G.add_edge(e[0], e[1])
nodes = list(G.nodes())
if isinstance(node_shape, str):
node_shape = {n: node_shape for n in nodes}
if isinstance(node_shape, dict):
missing_shapes = set(nodes) - set(node_shape.keys())
if missing_shapes:
raise ValueError("node_shape is missing entries for some nodes.")
if isinstance(node_size, np.ndarray):
if len(node_size) != len(nodes):
raise ValueError("node_size length must match number of nodes.")
node_size = {n: node_size[i] for i, n in enumerate(nodes)}
elif not isinstance(node_size, dict):
node_size = {n: node_size for n in nodes}
if isinstance(node_color, np.ndarray):
if len(node_color) != len(nodes):
raise ValueError("node_color length must match number of nodes.")
node_color = {n: node_color[i] for i, n in enumerate(nodes)}
elif not isinstance(node_color, dict):
node_color = {n: node_color for n in nodes}
if isinstance(node_facecolor, np.ndarray):
if len(node_facecolor) != len(nodes):
raise ValueError("node_facecolor length must match number of nodes.")
node_facecolor = {n: node_facecolor[i] for i, n in enumerate(nodes)}
elif not isinstance(node_facecolor, dict):
node_facecolor = {n: node_facecolor for n in nodes}
for n in nodes:
nx.draw_networkx_nodes(
G,
pos,
[n],
node_size=node_size[n],
node_shape=node_shape[n],
node_color=node_color[n],
edgecolors=node_facecolor[n],
ax=ax,
)
if with_node_labels:
ax.annotate(
n,
(pos[n][0] - 0.1, pos[n][1] - 0.06),
fontsize=label_size,
color=label_col,
)
# Plot the hyperedges (size>2/order>1).
for hye in list(hypergraph.get_edges()):
if len(hye) > 2:
points = []
for node in hye:
points.append((pos[node][0], pos[node][1]))
# Center of mass of points.
x_c = np.mean([x for x, y in points])
y_c = np.mean([y for x, y in points])
# Order points in a clockwise fashion.
points = sorted(points, key=lambda x: np.arctan2(x[1] - y_c, x[0] - x_c))
if len(points) == 3:
points = [
(x_c + 2.5 * (x - x_c), y_c + 2.5 * (y - y_c)) for x, y in points
]
else:
points = [
(x_c + 1.8 * (x - x_c), y_c + 1.8 * (y - y_c)) for x, y in points
]
Cartesian_coords_list = points + [points[0]]
obj = Object(Cartesian_coords_list)
Smoothed_obj = obj.Smooth_by_Chaikin(number_of_refinements=12)
# Visualisation.
x1 = [i for i, j in Smoothed_obj]
y1 = [j for i, j in Smoothed_obj]
order = len(hye) - 1
if order not in hyperedge_color_by_order.keys():
hyperedge_color_by_order[order] = _stable_color(order)
if order not in hyperedge_facecolor_by_order.keys():
hyperedge_facecolor_by_order[order] = _stable_color(order + 1000)
color = hyperedge_color_by_order[order]
facecolor = hyperedge_facecolor_by_order[order]
ax.fill(x1, y1, alpha=hyperedge_alpha, c=color, edgecolor=facecolor)
nx.draw_networkx_edges(G, pos, width=edge_width, edge_color=edge_color, ax=ax)
ax.axis("equal")
plt.axis("equal")
if show:
plt.show()
return ax