Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Drawing hypergraphs with hyperdedges as convex hulls #320

Merged
merged 30 commits into from
Apr 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
90e1a2a
Update draw.py
thomasrobiglio Mar 29, 2023
9a2e3be
Hypergraph hull drawing
thomasrobiglio Mar 29, 2023
3f13a7f
Update draw.py
thomasrobiglio Mar 29, 2023
3946482
update draw hull
thomasrobiglio Mar 29, 2023
abaddda
Update convex_hull_drawing_example.ipynb
thomasrobiglio Mar 29, 2023
9bbb354
Update draw.py
thomasrobiglio Mar 29, 2023
46651ef
Update convex_hull_drawing_example.ipynb
thomasrobiglio Mar 29, 2023
ca1451f
Update draw.py
thomasrobiglio Mar 30, 2023
a3338fd
Update draw.py
thomasrobiglio Mar 30, 2023
ee8efd5
Update draw.py
thomasrobiglio Mar 30, 2023
e7d27e3
Update draw.py
thomasrobiglio Mar 30, 2023
5b1e507
Update convex_hull_drawing_example.ipynb
thomasrobiglio Mar 30, 2023
f0c26c6
Update draw.py
thomasrobiglio Mar 30, 2023
9254433
Update convex_hull_drawing_example.ipynb
thomasrobiglio Mar 30, 2023
15329fb
Update convex_hull_drawing_example.ipynb
thomasrobiglio Mar 30, 2023
135c144
Update draw.py
thomasrobiglio Mar 31, 2023
8c849ce
update draw.py
thomasrobiglio Mar 31, 2023
371804e
updated tutorials
thomasrobiglio Mar 31, 2023
69fd3e3
Update convex_hull_drawing_example.ipynb
thomasrobiglio Mar 31, 2023
8d43cb8
Update Tutorial 5 - Plotting.ipynb
thomasrobiglio Mar 31, 2023
7a07eb6
Update draw.py
thomasrobiglio Mar 31, 2023
4827853
Update convex_hull_drawing_example.ipynb
thomasrobiglio Mar 31, 2023
e244a45
style: black and isort
maximelucas Apr 3, 2023
8e0ff24
minors cleaning
maximelucas Apr 3, 2023
e92c0ca
removed for loop for circles around nodes
thomasrobiglio Apr 4, 2023
06aa7e2
Update xgi/drawing/draw.py
thomasrobiglio Apr 4, 2023
1ad5b17
rename tutorial
thomasrobiglio Apr 4, 2023
855d6ee
Merge branch 'main' of https://github.com/thomasrobiglio/xgi
thomasrobiglio Apr 4, 2023
5c012cf
corrected error in faster implementation
thomasrobiglio Apr 4, 2023
64e92d5
add test for draw_hypergraph_hull
thomasrobiglio Apr 5, 2023
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
17 changes: 17 additions & 0 deletions tests/drawing/test_draw.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,3 +169,20 @@ def test_draw_simplices(edgelist8):
assert patch.get_zorder() == z

plt.close()

def test_draw_hypergraph_hull(edgelist8):

H = xgi.Hypergraph(edgelist8)

ax = xgi.draw_hypergraph_hull(H)

# number of elements
assert len(ax.patches) == len(H.edges.filterby("size", 2, mode="gt")) # hyperedges
assert len(ax.collections[0].get_sizes()) == H.num_nodes # nodes

# zorder
for patch, z in zip(ax.patches, [2, 2, 0, 2, 2]): # hyperedges
assert patch.get_zorder() == z
assert ax.collections[0].get_zorder() == 4 # nodes

plt.close()
6 changes: 2 additions & 4 deletions tutorials/Tutorial 5 - Plotting.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,6 @@
]
},
{
"attachments": {},
"cell_type": "markdown",
"id": "bad32b53",
"metadata": {},
Expand Down Expand Up @@ -227,7 +226,6 @@
]
},
{
"attachments": {},
"cell_type": "markdown",
"id": "fc7cf849",
"metadata": {},
Expand Down Expand Up @@ -672,7 +670,7 @@
],
"metadata": {
"kernelspec": {
"display_name": "hyper",
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
Expand All @@ -686,7 +684,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.0"
"version": "3.9.13"
}
},
"nbformat": 4,
Expand Down
260 changes: 260 additions & 0 deletions tutorials/Tutorial 7 - Convex hulls hypergraph plotting.ipynb

Large diffs are not rendered by default.

240 changes: 240 additions & 0 deletions xgi/drawing/draw.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import numpy as np
from matplotlib import cm
from matplotlib.colors import LinearSegmentedColormap, ListedColormap
from scipy.spatial import ConvexHull

from .. import convert
from ..classes import Hypergraph, SimplicialComplex, max_edge_order
Expand All @@ -22,6 +23,7 @@
"draw_simplices",
"draw_node_labels",
"draw_hyperedge_labels",
"draw_hypergraph_hull",
]


Expand Down Expand Up @@ -985,3 +987,241 @@ def _update_lims(pos, ax):
corners = (minx - padx, miny - pady), (maxx + padx, maxy + pady)
ax.update_datalim(corners)
ax.autoscale_view()


def _draw_hull(node_pos, ax, edges_ec, facecolor, alpha, zorder, radius):
"""Draw a convex hull encompassing the nodes in node_pos

Parameters
----------
node_pos : np.array
nx2 dimensional array containing positions of the nodes
ax : matplotlib.pyplot.axes
edges_ec : str
Color of the border of the convex hull
facecolor : str
Filling color of the convex hull
alpha : float
Transparency of the convex hull
radius : float
Radius of the convex hull in the vicinity of the nodes.

Returns
-------
ax : matplotlib.pyplot.axes

"""

thetas = np.linspace(0, 2*np.pi, num=100, endpoint=False)
offsets = radius * np.array([np.cos(thetas), np.sin(thetas)]).T
points = np.vstack([p + offsets for p in node_pos])
points = np.vstack([node_pos, points])

hull = ConvexHull(points)

for simplex in hull.simplices:
ax.plot(points[simplex, 0], points[simplex, 1], color=edges_ec, zorder=zorder)
ax.fill(
points[hull.vertices, 0],
points[hull.vertices, 1],
color=facecolor,
alpha=alpha,
zorder=zorder,
)

return ax


def draw_hypergraph_hull(
H,
pos=None,
ax=None,
dyad_color="black",
edge_fc=None,
edge_ec=None,
node_fc="tab:blue",
node_ec="black",
node_lw=1,
node_size=7,
max_order=None,
node_labels=False,
hyperedge_labels=False,
radius=0.05,
**kwargs,
):
"""Draw hypergraphs displaying the hyperedges of order k>1 as convex hulls


Parameters
----------
H : Hypergraph
pos : dict (default=None)
If passed, this dictionary of positions node_id:(x,y) is used for placing the nodes.
If None (default), use the `barycenter_spring_layout` to compute the positions.
ax : matplotlib.pyplot.axes (default=None)
dyad_color : str, dict, iterable, or EdgeStat (default='black')
Color of the dyadic links. If str, use the same color for all edges. If a dict, must
contain (edge_id: color_str) pairs. If iterable, assume the colors are
specified in the same order as the edges are found in H.edges. If EdgeStat, use a colormap
(specified with dyad_color_cmap) associated to it.
edge_fc : str, dict, iterable, or EdgeStat (default=None)
str, dict, iterable, or EdgeStat (default=None)
Color of the hyperedges of order k>1. If str, use the same color for all hyperedges of order k>1. If a dict, must
contain (edge_id: color_str) pairs. If other iterable, assume the colors are
specified in the same order as the hyperedges are found in H.edges. If EdgeStat,
use the colormap specified with edge_fc_cmap. If None (default),
use the H.edges.size.
edge_ec : str, dict, iterable, or EdgeStat (default=None)
Color of the borders of the hyperdges of order k>1. If str, use the same color for all edges. If a dict, must
contain (edge_id: color_str) pairs. If iterable, assume the colors are
specified in the same order as the edges are found in H.edges. If EdgeStat, use a colormap
(specified with edge_ec_cmap) associated to it. If None (default),
use the H.edges.size.
node_fc : node_fc : str, dict, iterable, or NodeStat (default='tab:blue')
Color of the nodes. If str, use the same color for all nodes. If a dict, must
contain (node_id: color_str) pairs. If other iterable, assume the colors are
specified in the same order as the nodes are found in H.nodes. If NodeStat,
use the colormap specified with node_fc_cmap.
node_ec : str, dict, iterable, or NodeStat (default='black')
Color of node borders. If str, use the same color for all nodes. If a dict, must
contain (node_id: color_str) pairs. If other iterable, assume the colors are
specified in the same order as the nodes are found in H.nodes. If NodeStat,
use the colormap specified with node_ec_cmap.
node_lw : int, float, dict, iterable, or EdgeStat (default=1)
Line width of the node borders in pixels. If int or float, use the same width for all node borders.
If a dict, must contain (node_id: width) pairs. If iterable, assume the widths are
specified in the same order as the nodes are found in H.nodes. If NodeStat, use a monotonic
linear interpolation defined between min_node_lw and max_node_lw.
node_size : int, float, dict, iterable, or NodeStat (default=7)
Radius of the nodes in pixels. If int or float, use the same radius for all nodes.
If a dict, must contain (node_id: radius) pairs. If iterable, assume the radiuses are
specified in the same order as the nodes are found in H.nodes. If NodeStat, use a monotonic
linear interpolation defined between min_node_size and max_node_size.
max_order : int (default=None)
Maximum of hyperedges to plot. If None (default), plots all orders.
node_labels : bool, or dict (default=False)
If True, draw ids on the nodes. If a dict, must contain (node_id: label) pairs.
hyperedge_labels : bool, or dict (default=False)
If True, draw ids on the hyperedges. If a dict, must contain (edge_id: label) pairs.
radius : float (deafault=0.05)
Radius of the convex hull in the vicinity of the nodes.
**kwargs : optional args
Alternate default values. Values that can be overwritten are the following:
* min_node_size
* max_node_size
* min_node_lw
* max_node_lw
* node_fc_cmap
* node_ec_cmap
* dyad_color_cmap
* edge_fc_cmap
* edge_ec_cmap
* alpha

Returns
-------
ax : matplotlib.pyplot.axes

"""

settings = {
"min_node_size": 5.0,
"max_node_size": 30.0,
"min_node_lw": 1.0,
"max_node_lw": 5.0,
"node_fc_cmap": cm.Reds,
"node_ec_cmap": cm.Greys,
"dyad_color_cmap": cm.Greys,
"edge_fc_cmap": cm.Blues,
"edge_ec_cmap": cm.Greys,
"alpha": 0.4,
}

alpha = settings["alpha"]

if edge_fc is None:
edge_fc = H.edges.size

edge_fc = _color_arg_to_dict(edge_fc, H.edges, settings["edge_fc_cmap"])

if edge_ec is None:
edge_ec = H.edges.size

edge_ec = _color_arg_to_dict(edge_ec, H.edges, settings["edge_ec_cmap"])

settings.update(kwargs)

if pos is None:
pos = barycenter_spring_layout(H)

if ax is None:
ax = plt.gca()

ax.get_xaxis().set_ticks([])
ax.get_yaxis().set_ticks([])
ax.axis("off")

if not max_order:
max_order = max_edge_order(H)

dyad_color = _color_arg_to_dict(dyad_color, H.edges, settings["dyad_color_cmap"])

for id, he in H._edge.items():
d = len(he) - 1
if d > max_order:
continue
if d == 1:
# Drawing the edges
he = list(he)
x_coords = [pos[he[0]][0], pos[he[1]][0]]
y_coords = [pos[he[0]][1], pos[he[1]][1]]
line = plt.Line2D(
x_coords,
y_coords,
color=dyad_color[id],
zorder=max_order - 1,
alpha=1,
)
ax.add_line(line)

else:
coordinates = [[pos[n][0], pos[n][1]] for n in he]
_draw_hull(
node_pos=np.array(coordinates),
ax=ax,
edges_ec=edge_ec[id],
facecolor=edge_fc[id],
alpha=alpha,
zorder=max_order - d,
radius=radius,
)

if hyperedge_labels:
# Get all valid keywords by inspecting the signatures of draw_node_labels
valid_label_kwds = signature(draw_hyperedge_labels).parameters.keys()
# Remove the arguments of this function (draw_networkx)
valid_label_kwds = valid_label_kwds - {"H", "pos", "ax", "hyperedge_labels"}
if any([k not in valid_label_kwds for k in kwargs]):
invalid_args = ", ".join([k for k in kwargs if k not in valid_label_kwds])
raise ValueError(f"Received invalid argument(s): {invalid_args}")
label_kwds = {k: v for k, v in kwargs.items() if k in valid_label_kwds}
draw_hyperedge_labels(H, pos, hyperedge_labels, ax_edges=ax, **label_kwds)

draw_nodes(
H,
pos,
ax,
node_fc,
node_ec,
node_lw,
node_size,
max_order,
settings,
node_labels,
**kwargs,
)

# compute axis limits
_update_lims(pos, ax)

return ax