Visualizing hypergraphs#
Visualizing hypergraphs, just like pairwise networks, is a hard task and no algorithm can work nicely for any given input structure. Here, we show how to visualize some toy structures using the visualization function contained in the drawing
module that is often inspired by networkx and relies on matplotlib.
[27]:
import matplotlib.pyplot as plt
import numpy as np
import xgi
Basics#
Les us first create a small toy hypergraph containing edges of different sizes.
[28]:
H = xgi.Hypergraph()
H.add_edges_from(
[[1, 2, 3], [3, 4, 5], [3, 6], [6, 7, 8, 9], [1, 4, 10, 11, 12], [1, 4]]
)
It can be quickly visualized simply with
[29]:
xgi.draw(H)
[29]:
(<Axes: >,
(<matplotlib.collections.PathCollection at 0x7719c8f11950>,
<matplotlib.collections.LineCollection at 0x7719cbb23810>,
<matplotlib.collections.PatchCollection at 0x7719c96fae50>))

By default, the layout is computed by xgi.barycenter_spring_layout
. For a bit more control, we can compute a layout externally (that we fix with a random seed), so that we can reuse it:
[30]:
pos = xgi.barycenter_spring_layout(H, seed=1)
fig, ax = plt.subplots()
xgi.draw(H, pos=pos, ax=ax)
[30]:
(<Axes: >,
(<matplotlib.collections.PathCollection at 0x7719c960fe50>,
<matplotlib.collections.LineCollection at 0x7719c96d3850>,
<matplotlib.collections.PatchCollection at 0x7719c9756390>))

Labels can be added to the nodes and hyperedges with arguments node_labels
and hyperedge_labels
. If True
, the IDs are shown. To display user-defined labels, pass a dictionary that contains (id: label). Additional keywords related to the font can be passed as well:
[31]:
xgi.draw(H, pos, node_labels=True, node_size=15, hyperedge_labels=True)
plt.show()

Note that by default, the nodes are drawn too small (size 7) to display the labels nicely. For better visuals, increase the node size to at least 15 when displaying node labels.
Other types of visualizations#
Another common way of visualizing hypergraph is with convex hulls as hyperedges. This can be done simply by setting hull=True
:
[32]:
fig, ax = plt.subplots()
xgi.draw(H, pos=pos, ax=ax, hull=True)
plt.show()

Or with colors only on the contours:
[33]:
fig, ax = plt.subplots()
xgi.draw(H, pos=pos, ax=ax, hull=True, edge_fc="white")
plt.show()

XGI also offer a function to visualize a hypergraph as a multilayer, where each layer contains hyperedges of a given order:
[34]:
ax3 = plt.axes(projection="3d") # requires a 3d axis
xgi.draw_multilayer(H, ax=ax3)
plt.show()

Colors and sizes#
The drawing functions offer great flexibility in choosing the width, size, and color of the elements that are drawn.
By default, hyperedges are colored according to their order. Hyperedge and node colors can be set manually to a single color, a list of colors, or a by an array/dict/Stat of numerical values.
In the latter case, numerical values are mapped to colors via a colormap which can be changed manually (see colormaps for details):
First, let’s use single values:
[35]:
fig, ax = plt.subplots()
xgi.draw(
H,
pos=pos,
ax=ax,
node_fc="k",
node_ec="beige",
node_shape="s",
node_size=10,
node_lw=2,
edge_fc="salmon",
dyad_color="grey",
dyad_lw=3,
)
plt.show()

Now we can use statistic to set the colors and sizes, and change the colormaps:
[36]:
fig, ax = plt.subplots(figsize=(6, 2.5))
ax, collections = xgi.draw(
H,
pos=pos,
node_fc=H.nodes.degree,
edge_fc=H.edges.size,
edge_fc_cmap="viridis",
node_fc_cmap="mako_r",
)
node_col, _, edge_col = collections
plt.colorbar(node_col, label="Node degree")
plt.colorbar(edge_col, label="Edge size")
plt.show()

Layouts#
Other layout algorithms are available:
random_layout
: to position nodes uniformly at random in the unit square (exactly as networkx).pairwise_spring_layout
: to position the nodes using the Fruchterman-Reingold force-directed algorithm on the projected graph. In this case the hypergraph is first projected into a graph (1-skeleton) using thexgi.convert_to_graph(H)
function and then networkx’s spring_layout is applied.barycenter_spring_layout
: to position the nodes using the Fruchterman-Reingold force-directed algorithm using an augmented version of the the graph projection of the hypergraph, where phantom nodes (that we call barycenters) are created for each edge of order \(d>1\) (composed by more than two nodes). Weights are then assigned to all hyperedges of order 1 (links) and to all connections to phantom nodes within each hyperedge to keep them together. Weights scale with the size of the hyperedges. Finally, the weighted version of networkx’s spring_layout is applied.weighted_barycenter_spring_layout
: same asbarycenter_spring_layout
, but here the weighted version of the Fruchterman-Reingold force-directed algorithm is used. Weights are assigned to all hyperedges of order 1 (links) and to all connections to phantom nodes within each hyperedge to keep them together. Weights scale with the order of the group interaction.
Each layout returns a dictionary that maps nodes ID into (x, y) coordinates.
For example:
[37]:
plt.figure(figsize=(10, 4))
ax = plt.subplot(1, 2, 1)
pos_circular = xgi.circular_layout(H)
xgi.draw(H, pos=pos_circular, ax=ax)
ax = plt.subplot(1, 2, 2)
pos_spiral = xgi.spiral_layout(H)
xgi.draw(H, pos=pos_spiral, ax=ax)
[37]:
(<Axes: >,
(<matplotlib.collections.PathCollection at 0x7719c8d56990>,
<matplotlib.collections.LineCollection at 0x7719c8d35f90>,
<matplotlib.collections.PatchCollection at 0x7719c8d78990>))

Simplicial complexes#
Simplicial complexes can be visualized using the same functions as for Hypergraphs.
Technical note: By definition, a simplicial complex object contains all sub-simplices. This would make the visualization heavy since all sub-simplices contained in a maximal simplex would overlap. The automatic solution for this, implemented by default in all the layouts, is to convert the simplicial complex into a hypergraph composed solely by its maximal simplices.
Visual note: To visually distinguish simplicial complexes from hypergraphs, the draw
function will also show all links contained in each maximal simplices (while omitting simplices of intermediate orders).
[38]:
SC = xgi.SimplicialComplex()
SC.add_simplices_from([[3, 4, 5], [3, 6], [6, 7, 8, 9], [1, 4, 10, 11, 12], [1, 4]])
[39]:
pos = xgi.barycenter_spring_layout(SC, seed=1)
[40]:
xgi.draw(SC, pos=pos)
plt.show()

DiHypergraphs#
We visualize dihypergraphs as directed bipartite graphs.
[41]:
diedges = [({0, 1}, {2}), ({1}, {4}), ({2, 3}, {4, 5})]
DH = xgi.DiHypergraph(diedges)
[42]:
xgi.draw_bipartite(DH)
[42]:
(<Axes: >,
(<matplotlib.collections.PathCollection at 0x7719c8df6e90>,
<matplotlib.collections.PathCollection at 0x7719c8d28350>))

Drawing hypergraphs as bipartite graphs#
Not only can we draw dihypergraphs as bipartite graphs, but we can also draw undirected hypergraphs as bipartite graphs.
[43]:
xgi.draw_bipartite(H)
[43]:
(<Axes: >,
(<matplotlib.collections.PathCollection at 0x7719c8916650>,
<matplotlib.collections.PathCollection at 0x7719c8957250>,
<matplotlib.collections.LineCollection at 0x7719c8fd6f50>))

Now, instead of a single dictionary specifying node positions, we must specify both node and edge marker positions. The bipartite_spring_layout
method returns a tuple of dictionaries:
[44]:
pos = xgi.bipartite_spring_layout(H)
pos
[44]:
({1: array([-0.13869776, 0.42720772]),
2: array([-0.47057658, 0.22970862]),
3: array([-0.14113591, -0.0524324 ]),
4: array([-0.03307574, 0.38826275]),
5: array([-0.28548812, 0.02956658]),
6: array([ 0.0755744 , -0.59066535]),
7: array([ 0.28313206, -1. ]),
8: array([ 0.37399109, -0.86556338]),
9: array([ 0.11561793, -0.99118983]),
10: array([0.20529259, 0.73957492]),
11: array([0.26597268, 0.6043045 ]),
12: array([0.05293619, 0.78627083])},
{0: array([-0.26617907, 0.20015611]),
1: array([-0.12324552, 0.14065297]),
2: array([-0.04010974, -0.33391482]),
3: array([ 0.19907022, -0.83064578]),
4: array([0.06938272, 0.58448117]),
5: array([-0.14246145, 0.53422539])})
We can change the style of any of the plot elements just as we can for draw
. In addition, we can use any of the layouts described above with the edge_positions_from_barycenters
function, to place the edge markers directly between their constituent nodes.
[45]:
node_pos = xgi.circular_layout(H)
edge_pos = xgi.edge_positions_from_barycenters(H, node_pos)
xgi.draw_bipartite(
H,
node_shape="p",
node_size=10,
edge_marker_size=15,
dyad_style="dashed",
pos=(node_pos, edge_pos),
)
[45]:
(<Axes: >,
(<matplotlib.collections.PathCollection at 0x7719ca777fd0>,
<matplotlib.collections.PathCollection at 0x7719c89a6810>,
<matplotlib.collections.LineCollection at 0x7719c8977a50>))

Rotating a hypergraph#
For some hypergraphs, it can be helpful to rotate the positions of the nodes relative to the principal axis. We can do this by generating node positions with any of the functions previously described and then using the function pca_transform()
. For example:
[46]:
pos = xgi.barycenter_spring_layout(H, seed=1)
transformed_pos = xgi.pca_transform(pos)
xgi.draw(H, transformed_pos)
plt.show()

We can also rotate the node positions relative to the principal axis:
[47]:
# rotation in degrees
transformed_pos = xgi.pca_transform(pos, 30)
xgi.draw(H, transformed_pos)
plt.show()

Larger example: generative model#
We generate and visualize a random hypergraph.
[48]:
n = 100
is_connected = False
while not is_connected:
H_random = xgi.random_hypergraph(n, [0.03, 0.0002, 0.00001])
is_connected = xgi.is_connected(H_random)
pos = xgi.barycenter_spring_layout(H_random, seed=1)
/home/lucasm/WORK/SCIENCE/xgi/xgi/generators/random.py:154: UserWarning: This method is much slower than fast_random_hypergraph
warn("This method is much slower than fast_random_hypergraph")
Since there are more nodes we reduce the node_size
.
[49]:
plt.figure(figsize=(10, 10))
ax = plt.subplot(111)
xgi.draw(H_random, pos=pos, ax=ax)
[49]:
(<Axes: >,
(<matplotlib.collections.PathCollection at 0x7719c8f87090>,
<matplotlib.collections.LineCollection at 0x7719c7f94190>,
<matplotlib.collections.PatchCollection at 0x7719c7f1b0d0>))

We can even size/color the nodes and edges by NodeStats or EdgeStats (e.g., degree, centrality, size, etc.)!
[50]:
plt.figure(figsize=(10, 10))
ax = plt.subplot(111)
xgi.draw(
H_random,
pos=pos,
ax=ax,
node_size=H_random.nodes.degree,
node_fc=H_random.nodes.clique_eigenvector_centrality,
)
[50]:
(<Axes: >,
(<matplotlib.collections.PathCollection at 0x7719c8dad5d0>,
<matplotlib.collections.LineCollection at 0x7719cc5a4910>,
<matplotlib.collections.PatchCollection at 0x7719c89a5150>))

Degree distribution#
Using its simplest (higher-order) definition, the degree is the number of hyperedges (of any size) incident on a node.
[51]:
centers, heights = xgi.degree_histogram(H_random)
plt.figure(figsize=(12, 4))
ax = plt.subplot(111)
ax.bar(centers, heights)
ax.set_ylabel("Count")
ax.set_xlabel("Degree")
ax.set_xticks(np.arange(1, max(centers) + 1, step=1));

[52]:
plt.close("all")