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>))
../../_images/api_tutorials_focus_5_7_1.png

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>))
../../_images/api_tutorials_focus_5_9_1.png

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()
../../_images/api_tutorials_focus_5_11_0.png

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()
../../_images/api_tutorials_focus_5_15_0.png

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()
../../_images/api_tutorials_focus_5_17_0.png

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()
../../_images/api_tutorials_focus_5_19_0.png

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()
../../_images/api_tutorials_focus_5_23_0.png

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()
../../_images/api_tutorials_focus_5_25_0.png

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 the xgi.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 as barycenter_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>))
../../_images/api_tutorials_focus_5_29_1.png

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()
../../_images/api_tutorials_focus_5_34_0.png

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>))
../../_images/api_tutorials_focus_5_37_1.png

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>))
../../_images/api_tutorials_focus_5_39_1.png

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>))
../../_images/api_tutorials_focus_5_43_1.png

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()
../../_images/api_tutorials_focus_5_46_0.png

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()
../../_images/api_tutorials_focus_5_48_0.png

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>))
../../_images/api_tutorials_focus_5_52_1.png

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>))
../../_images/api_tutorials_focus_5_54_1.png

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));
../../_images/api_tutorials_focus_5_56_0.png
[52]:
plt.close("all")