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

Add the ability to draw hypergraphs as a bipartite graph #492

Merged
merged 45 commits into from
Apr 22, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
97013fe
add bipartite layout
nwlandry Oct 18, 2023
b41067f
Update draw.py
nwlandry Oct 18, 2023
8614660
update
nwlandry Nov 3, 2023
f4d6760
remove unused imports
nwlandry Nov 3, 2023
d660bc4
updates
nwlandry Jan 14, 2024
925c022
fix import error
nwlandry Feb 12, 2024
5d053b3
update with reduced functionality
nwlandry Feb 12, 2024
0185f2b
Update draw.py
nwlandry Feb 12, 2024
4f82531
fix unit tests and functionality
nwlandry Feb 13, 2024
c5dec10
removed unnecessary imports
nwlandry Feb 13, 2024
c21fe77
removed references to old drawing function
nwlandry Feb 13, 2024
6f64151
formatting
nwlandry Feb 13, 2024
557b82f
add instructions
nwlandry Feb 13, 2024
5096a79
Update HOW_TO_CONTRIBUTE.md
nwlandry Feb 13, 2024
2e34fb3
Update developer.txt
nwlandry Feb 13, 2024
41efd3e
Fix messy notebook diffs. Merge branch 'main' into add-draw-bipartite
nwlandry Feb 13, 2024
d5adfa7
add tests
nwlandry Feb 26, 2024
48e27f4
fix api and documentation
nwlandry Feb 26, 2024
98088fd
Merge branch 'main' into add-draw-bipartite
nwlandry Feb 26, 2024
a069b11
updated changelog and upversion
nwlandry Feb 29, 2024
7848f52
Revert "updated changelog and upversion"
nwlandry Feb 29, 2024
8548006
updates
nwlandry Mar 2, 2024
e931e8b
response to review
nwlandry Mar 2, 2024
66994ce
updated kernel
nwlandry Mar 8, 2024
249c371
Update Tutorial 7 - Directed Hypergraphs.ipynb
nwlandry Mar 8, 2024
08f6c06
updates
nwlandry Mar 8, 2024
7934570
Update docs
nwlandry Mar 8, 2024
9325619
Fix failing tests
nwlandry Mar 8, 2024
88c01b2
Merge branch 'main' into add-draw-bipartite
nwlandry Mar 8, 2024
105fd9b
Removed bad looking drawings
nwlandry Mar 8, 2024
950427a
added unit tests and updated code to match
nwlandry Mar 8, 2024
253e844
fixed a bug
nwlandry Mar 8, 2024
1307d55
response to review
nwlandry Mar 11, 2024
bf092f3
Added explanatory comment
nwlandry Apr 2, 2024
a2b90ae
Update xgi/drawing/draw.py
nwlandry Apr 17, 2024
7e2970b
Update xgi/drawing/draw.py
nwlandry Apr 18, 2024
9f2c6eb
Update xgi/drawing/draw.py
nwlandry Apr 18, 2024
d7c4554
Response to review
nwlandry Apr 18, 2024
acd7071
Remove broken conventional commits link
nwlandry Apr 18, 2024
6ffce2f
Update Tutorial 5 - Plotting.ipynb
nwlandry Apr 18, 2024
330f865
added and fixed tests
nwlandry Apr 18, 2024
c42a946
added documentation
nwlandry Apr 18, 2024
909489d
Fixed colormap
nwlandry Apr 22, 2024
362a591
fixed edge color issue.
nwlandry Apr 22, 2024
730bd74
Merge branch 'main' into add-draw-bipartite
nwlandry Apr 22, 2024
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
6 changes: 1 addition & 5 deletions tests/drawing/test_draw_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,7 @@
from matplotlib import cm

import xgi
from xgi.drawing.draw import (
_CCW_sort,
_draw_arg_to_arr,
_interp_draw_arg,
)
from xgi.drawing.draw import _CCW_sort, _draw_arg_to_arr, _interp_draw_arg


def test_CCW_sort():
Expand Down
243 changes: 238 additions & 5 deletions xgi/drawing/draw.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import matplotlib as mpl
import matplotlib.pyplot as plt
import numpy as np
import seaborn as sb # for cmap "crest"
nwlandry marked this conversation as resolved.
Show resolved Hide resolved
from matplotlib import cm
from matplotlib.patches import FancyArrowPatch
from mpl_toolkits.mplot3d.art3d import (
Expand All @@ -19,6 +18,7 @@

from .. import convert
from ..algorithms import max_edge_order, unique_edge_sizes
from ..convert import to_bipartite_edgelist
from ..core import DiHypergraph, Hypergraph, SimplicialComplex
from ..exception import XGIError
from ..utils import subfaces
Expand All @@ -30,7 +30,11 @@
_parse_color_arg,
_update_lims,
)
from .layout import _augmented_projection, barycenter_spring_layout
from .layout import (
_augmented_projection,
barycenter_spring_layout,
bipartite_spring_layout,
)

__all__ = [
"draw",
Expand All @@ -41,6 +45,7 @@
"draw_hyperedge_labels",
"draw_multilayer",
"draw_dihypergraph",
"draw_bipartite",
]


Expand Down Expand Up @@ -392,7 +397,7 @@ def draw_nodes(
* "max_node_size" (default: 30)
* "min_node_lw" (default: 0)
* "max_node_lw" (default: 5)

kwargs : optional keywords
See `draw_node_labels` for a description of optional keywords.

Expand Down Expand Up @@ -595,7 +600,7 @@ def draw_hyperedges(
Default parameters. Keys that may be useful to override default settings:
* "min_dyad_lw" (default: 1)
* "max_dyad_lw" (default: 10)

kwargs : optional keywords
See `draw_hyperedge_labels` for a description of optional keywords.

Expand Down Expand Up @@ -839,7 +844,7 @@ def draw_simplices(
Default parameters. Keys that may be useful to override default settings:
* "min_dyad_lw" (default: 1)
* "max_dyad_lw" (default: 10)

kwargs : optional keywords
See `draw_hyperedge_labels` for a description of optional keywords.

Expand Down Expand Up @@ -1916,3 +1921,231 @@ def to_marker_edge(marker_size, marker):
_update_lims(pos_nodes, ax)

return ax, (node_collection, phantom_node_collection)


def draw_bipartite(
H,
node_pos=None,
edge_pos=None,
ax=None,
node_fc="white",
node_ec="black",
node_shape="o",
node_lw=1,
node_size=10,
edge_marker_fc="blue",
edge_marker_ec="black",
edge_marker_shape="s",
edge_marker_lw=1,
edge_marker_size=10,
dyad_color="black",
dyad_lw=1,
max_order=None,
rescale_sizes=True,
**kwargs,
):
"""Draw a hypergraph as a bipartite network.

Parameters
----------
H : Hypergraph
The hypergraph to draw.
ax : matplotlib.pyplot.axes, optional
Axis to draw on. If None (default), get the current axes.
lines_fc : str, dict, iterable, optional
Color of the hyperedges (lines). If str, use the same color for all hyperedges.
If a dict, must contain (hyperedge_id: color_str) pairs. If other iterable,
assume the colors are specified in the same order as the hyperedges are found
in DH.edges. If None (default), use the size of the hyperedges.
lines_lw : int, float, dict, iterable, optional
Line width of the hyperedges (lines). If int or float, use the same width for
all hyperedges. If a dict, must contain (hyperedge_id: width) pairs. If other
iterable, assume the widths are specified in the same order as the hyperedges
are found in DH.edges. By default, 1.5.
line_head_width : float, optional
Length of arrows' heads. By default, 0.05
node_fc : str, dict, iterable, or NodeStat, optional
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. By default, "white".
node_ec : str, dict, iterable, or NodeStat, optional
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. By default, "black".
node_lw : int, float, dict, iterable, or NodeStat, optional
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. By default, 1.
node_size : int, float, dict, iterable, or NodeStat, optional
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. By default, 15.
edge_marker_toggle: bool, optional
If True then marker representing the hyperedges are drawn. By default True.
edge_marker_fc: str, dict, iterable, optional
Filling color of the hyperedges (markers). If str, use the same color for all hyperedges.
If a dict, must contain (hyperedge_id: color_str) pairs. If other iterable,
assume the colors are specified in the same order as the hyperedges are found
in DH.edges. If None (default), use the size of the hyperedges.
edge_marker_ec: str, dict, iterable, optional
Edge color of the hyperedges (markers). If str, use the same color for all hyperedges.
If a dict, must contain (hyperedge_id: color_str) pairs. If other iterable,
assume the colors are specified in the same order as the hyperedges are found
in DH.edges. If None (default), use the size of the hyperedges.
edge_marker: str, optional
Marker used for the hyperedges. By default 's' (square marker).
max_order : int, optional
Maximum of hyperedges to plot. If None (default), plots all orders.
node_labels : bool or dict, optional
If True, draw ids on the nodes. If a dict, must contain (node_id: label) pairs.
By default, False.
hyperedge_labels : bool or dict, optional
If True, draw ids on the hyperedges. If a dict, must contain (edge_id: label)
pairs. By default, False.
**kwargs : optional args
Alternate default values. Values that can be overwritten are the following:
* min_node_size
nwlandry marked this conversation as resolved.
Show resolved Hide resolved
* max_node_size
* min_node_lw
* max_node_lw
* min_edge_marker_size
* max_edge_marker_size
* min_edge_marker_lw
* max_edge_marker_lw
* node_fc_cmap
nwlandry marked this conversation as resolved.
Show resolved Hide resolved
* node_ec_cmap
nwlandry marked this conversation as resolved.
Show resolved Hide resolved
* edge_marker_fc_cmap
nwlandry marked this conversation as resolved.
Show resolved Hide resolved
* edge_marker_ec_cmap
nwlandry marked this conversation as resolved.
Show resolved Hide resolved
* min_dyad_lw
* max_dyad_lw
* dyad_color_cmap
nwlandry marked this conversation as resolved.
Show resolved Hide resolved


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

Raises
------
XGIError
If something different than a DiHypergraph is passed.

See Also
--------
draw
draw_nodes
draw_node_labels

"""
if not isinstance(H, Hypergraph):
raise XGIError("The input must be a Hypergraph")

settings = {
"min_node_lw": 10.0,
"max_node_lw": 30.0,
"min_node_size": 10.0,
"max_node_size": 30.0,
"min_edge_marker_lw": 10.0,
"max_edge_marker_lw": 30.0,
"min_edge_marker_size": 10.0,
"max_edge_marker_size": 30.0,
"min_dyad_lw": 1.0,
"max_dyad_lw": 5.0,
"node_fc_cmap": cm.Reds,
"node_ec_cmap": cm.RdBu,
"dyad_color_cmap": cm.Blues,
"edge_marker_fc_cmap": cm.Greys,
"edge_marker_ec_cmap": cm.Blues,
nwlandry marked this conversation as resolved.
Show resolved Hide resolved
}

settings.update(kwargs)

node_settings = {
"min_node_lw": settings["min_node_lw"],
"max_node_lw": settings["max_node_lw"],
"min_node_size": settings["min_node_size"],
"max_node_size": settings["max_node_size"],
"node_fc_cmap": settings["node_fc_cmap"],
"node_ec_cmap": settings["node_ec_cmap"],
nwlandry marked this conversation as resolved.
Show resolved Hide resolved
}

edge_marker_settings = {
"min_node_lw": settings["min_edge_marker_lw"],
"max_node_lw": settings["max_edge_marker_lw"],
"min_node_size": settings["min_edge_marker_size"],
"max_node_size": settings["max_edge_marker_size"],
"node_fc_cmap": settings["edge_marker_fc_cmap"],
"node_ec_cmap": settings["edge_marker_ec_cmap"],
nwlandry marked this conversation as resolved.
Show resolved Hide resolved
}

if not node_pos or not edge_pos:
node_pos, edge_pos = bipartite_spring_layout(H)

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

ax, node_collection = draw_nodes(
H=H,
pos=node_pos,
ax=ax,
node_fc=node_fc,
node_ec=node_ec,
node_lw=node_lw,
node_size=node_size,
node_shape=node_shape,
zorder=2,
params=node_settings,
node_labels=None,
rescale_sizes=rescale_sizes,
**kwargs,
)

ax, edge_marker_collection = draw_nodes(
H=H.dual(),
pos=edge_pos,
ax=ax,
node_fc=edge_marker_fc,
node_ec=edge_marker_ec,
node_lw=edge_marker_lw,
node_size=edge_marker_size,
node_shape=edge_marker_shape,
zorder=1,
params=edge_marker_settings,
node_labels=None,
rescale_sizes=rescale_sizes,
**kwargs,
)

dyads = to_bipartite_edgelist(H)
dyad_lw = _draw_arg_to_arr(dyad_lw)
# dyad_color, dyad_c_mapped = _parse_color_arg(dyad_color, list(dyads)
# check validity of input values
if np.any(dyad_lw < 0):
raise ValueError("dyad_lw cannot contain negative values.")

# interpolate if needed
if rescale_sizes and isinstance(dyad_lw, np.ndarray):
dyad_lw = _interp_draw_arg(
dyad_lw, settings["min_dyad_lw"], settings["max_dyad_lw"]
)

dyad_pos = np.asarray([(node_pos[list(e)[0]], edge_pos[list(e)[1]]) for e in dyads])

dyad_collection = LineCollection(
dyad_pos,
colors=dyad_color,
linewidths=dyad_lw,
antialiaseds=(1,),
cmap=settings["dyad_color_cmap"],
zorder=0,
)

ax.add_collection(dyad_collection)

return ax, (node_collection, edge_marker_collection, dyad_collection)
13 changes: 2 additions & 11 deletions xgi/drawing/draw_utils.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,9 @@
"""Draw hypergraphs and simplicial complexes with matplotlib."""

from collections.abc import Iterable

import matplotlib.pyplot as plt
import numpy as np
from matplotlib.colors import (
LinearSegmentedColormap,
ListedColormap,
is_color_like,
to_rgba_array,
)
from numpy import ndarray

from ..exception import XGIError
from matplotlib.colors import is_color_like, to_rgba_array

from ..stats import EdgeStat, IDStat, NodeStat
from .layout import barycenter_spring_layout

Expand Down
Loading
Loading