# Copyright © 2018 Battelle Memorial Institute
# All rights reserved.
import matplotlib.pyplot as plt
from matplotlib.collections import LineCollection
import networkx as nx
from hypernetx.drawing.util import get_frozenset_label
[docs]
def layout_two_column(H, spacing=2):
"""
Two column (bipartite) layout algorithm.
This algorithm first converts the hypergraph into a bipartite graph and
then computes connected components. Disonneccted components are handled
independently and then stacked together.
Within a connected component, the spectral ordering of the bipartite graph
provides a quick and dirty ordering that minimizes edge crossings in the
diagram.
Parameters
----------
H: hnx.Hypergraph
the entity to be drawn
spacing: float
amount of whitespace between disconnected components
"""
offset = 0
pos = {}
def stack(vertices, x, height):
for i, v in enumerate(vertices):
pos[v] = (x, i + offset + (height - len(vertices)) / 2)
G = H.bipartite()
for ci in nx.connected_components(G):
Gi = G.subgraph(ci)
key = {v: i for i, v in enumerate(nx.spectral_ordering(Gi))}.get
ci_vertices, ci_edges = [
sorted([v for v, d in Gi.nodes(data=True) if d["bipartite"] == j], key=key)
for j in [0, 1]
]
height = max(len(ci_vertices), len(ci_edges))
stack(ci_vertices, 0, height)
stack(ci_edges, 1, height)
offset += height + spacing
return pos
[docs]
def draw_hyper_edges(H, pos, ax=None, **kwargs):
"""
Renders hyper edges for the two column layout.
Each node-hyper edge membership is rendered as a line connecting the node
in the left column to the edge in the right column.
Parameters
----------
H: hnx.Hypergraph
the entity to be drawn
pos: dict
mapping of node and edge positions to R^2
ax: Axis
matplotlib axis on which the plot is rendered
kwargs: dict
keyword arguments passed to matplotlib.LineCollection
Returns
-------
LineCollection
the hyper edges
"""
ax = ax or plt.gca()
pairs = [(v, e) for e in H.edges() for v in H.edges[e]]
kwargs = {
k: v if type(v) != dict else [v.get(e) for _, e in pairs]
for k, v in kwargs.items()
}
lines = LineCollection([(pos[u], pos[v]) for u, v in pairs], **kwargs)
ax.add_collection(lines)
return lines
[docs]
def draw_hyper_labels(
H, pos, labels={}, with_node_labels=True, with_edge_labels=True, ax=None
):
"""
Renders hyper labels (nodes and edges) for the two column layout.
Parameters
----------
H: hnx.Hypergraph
the entity to be drawn
pos: dict
mapping of node and edge positions to R^2
labels: dict
custom labels for nodes and edges can be supplied
with_node_labels: bool
False to disable node labels
with_edge_labels: bool
False to disable edge labels
ax: Axis
matplotlib axis on which the plot is rendered
kwargs: dict
keyword arguments passed to matplotlib.LineCollection
"""
ax = ax or plt.gca()
to_draw = []
if with_node_labels:
to_draw.append((list(H.nodes()), "right"))
if with_edge_labels:
to_draw.append((list(H.edges()), "left"))
for points, ha in to_draw:
for p in points:
ax.annotate(labels.get(p, p), pos[p], ha=ha, va="center")
[docs]
def draw(
H,
with_node_labels=True,
with_edge_labels=True,
with_node_counts=False,
with_edge_counts=False,
with_color=True,
edge_kwargs=None,
ax=None,
):
"""
Draw a hypergraph using a two-collumn layout.
This is intended reproduce an illustrative technique for bipartite graphs
and hypergraphs that is typically used in papers and textbooks.
The left column is reserved for nodes and the right column is reserved for
edges. A line is drawn between a node an an edge
The order of nodes and edges is optimized to reduce line crossings between
the two columns. Spacing between disconnected components is adjusted to make
the diagram easier to read, by reducing the angle of the lines.
Parameters
----------
H: hnx.Hypergraph
the entity to be drawn
with_node_labels: bool
False to disable node labels
with_edge_labels: bool
False to disable edge labels
with_node_counts: bool
set to True to label collapsed nodes with number of elements
with_edge_counts: bool
set to True to label collapsed edges with number of elements
with_color: bool
set to False to disable color cycling of hyper edges
edge_kwargs: dict
keyword arguments to pass to matplotlib.LineCollection
ax: Axis
matplotlib axis on which the plot is rendered
"""
edge_kwargs = edge_kwargs or {}
ax = ax or plt.gca()
pos = layout_two_column(H)
V = [v for v in H.nodes()]
E = [e for e in H.edges()]
labels = {}
labels.update(get_frozenset_label(V, count=with_node_counts))
labels.update(get_frozenset_label(E, count=with_edge_counts))
if with_color:
edge_kwargs["color"] = {
e: plt.cm.tab10(i % 10) for i, e in enumerate(H.edges())
}
draw_hyper_edges(H, pos, ax=ax, **edge_kwargs)
draw_hyper_labels(
H,
pos,
labels,
ax=ax,
with_node_labels=with_node_labels,
with_edge_labels=with_edge_labels,
)
ax.autoscale_view()
ax.axis("off")