Skip to content

Commit

Permalink
Add the ability to draw hypergraphs as a bipartite graph (#492)
Browse files Browse the repository at this point in the history
* add bipartite layout

* Update draw.py

* update

* remove unused imports

* updates

* fix import error

* update with reduced functionality

* Update draw.py

* fix unit tests and functionality

* removed unnecessary imports

* removed references to old drawing function

* formatting

* add instructions

* Update HOW_TO_CONTRIBUTE.md

* Update developer.txt

* add tests

* fix api and documentation

* updated changelog and upversion

* Revert "updated changelog and upversion"

This reverts commit a069b11.

* updates

* response to review

* updated kernel

* Update Tutorial 7 - Directed Hypergraphs.ipynb

* updates

* Update docs

* Fix failing tests

* Removed bad looking drawings

* added unit tests and updated code to match

* fixed a bug

* response to review

* Added explanatory comment

* Update xgi/drawing/draw.py

Co-authored-by: Maxime Lucas <maximelucas@users.noreply.github.com>

* Update xgi/drawing/draw.py

Co-authored-by: Maxime Lucas <maximelucas@users.noreply.github.com>

* Update xgi/drawing/draw.py

Co-authored-by: Maxime Lucas <maximelucas@users.noreply.github.com>

* Response to review

* Remove broken conventional commits link

* Update Tutorial 5 - Plotting.ipynb

* added and fixed tests

* added documentation

* Fixed colormap

* fixed edge color issue.

---------

Co-authored-by: Maxime Lucas <maximelucas@users.noreply.github.com>
  • Loading branch information
nwlandry and maximelucas authored Apr 22, 2024
1 parent 0f9be8d commit 46f3be3
Show file tree
Hide file tree
Showing 20 changed files with 1,344 additions and 553 deletions.
19 changes: 9 additions & 10 deletions HOW_TO_CONTRIBUTE.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,15 @@ Please note we have a [code of conduct](/CODE_OF_CONDUCT.md), please follow it i
## Pull Request process

1. Download the dependencies in the developer [requirements file](/requirements/developer.txt).
2. [Optional, but STRONGLY preferred] Label commits according to [Conventional Commits](https://www.conventionalcommits.org) style.
3. [Optional, but STRONGLY preferred] Add unit tests for features being added or bugs being fixed.
4. [Optional, but STRONGLY preferred] Include any new method/function in the corresponding docs file.
5. Run `pytest` to verify all unit tests pass.
6. [Optional, but STRONGLY preferred] Run `pylint xgi/ --disable all --enable W0611` and remove any unnecessary dependencies.
7. [Optional, but STRONGLY preferred] Run `isort .` and `nbqa isort .` to sort any new import statements in the source code and tutorials.
8. [Optional, but STRONGLY preferred] Run `black .` for consistent styling.
9. Update the "Current Version" section of CHANGELOG.md with overview of changes to the interface and add the usernames of all contributors.
10. Submit Pull Request with a list of changes, links to issues that it addresses (if applicable)
11. You may merge the Pull Request in once you have the sign-off of at least one other developer, or if you do not have permission to do that, you may request the reviewer to merge it for you.
2. [Optional, but STRONGLY preferred] Add unit tests for features being added or bugs being fixed.
3. [Optional, but STRONGLY preferred] Include any new method/function in the corresponding docs file.
4. Run `pytest` to verify all unit tests pass.
5. [Optional, but STRONGLY preferred] Run `pylint xgi/ --disable all --enable W0611` and `nbqa pylint . --disable all --enable W0611` and remove any unnecessary dependencies.
6. [Optional, but STRONGLY preferred] Run `isort .` and `nbqa isort .` to sort any new import statements in the source code and tutorials.
7. [Optional, but STRONGLY preferred] Run `black .` for consistent styling.
8. Update the "Current Version" section of CHANGELOG.md with overview of changes to the interface and add the usernames of all contributors.
9. Submit Pull Request with a list of changes, links to issues that it addresses (if applicable)
10. You may merge the Pull Request in once you have the sign-off of at least one other developer, or if you do not have permission to do that, you may request the reviewer to merge it for you.

## New Version process
1. Make sure that the Github Actions workflow runs without any errors.
Expand Down
3 changes: 1 addition & 2 deletions benchmarks/hypernetx.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
"import time\n",
"\n",
"import hypernetx as hnx\n",
"import pandas as pd\n",
"\n",
"import xgi"
]
Expand Down Expand Up @@ -223,7 +222,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.6 | packaged by conda-forge | (main, Aug 22 2022, 20:38:29) [Clang 13.0.1 ]"
"version": "3.10.0"
},
"vscode": {
"interpreter": {
Expand Down
2 changes: 1 addition & 1 deletion benchmarks/networkx.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.4"
"version": "3.10.0"
},
"orig_nbformat": 4
},
Expand Down
4 changes: 3 additions & 1 deletion docs/source/api/drawing/xgi.drawing.draw.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ xgi.drawing.draw
.. rubric:: Functions

.. autofunction:: draw
.. autofunction:: draw_dihypergraph
.. autofunction:: draw_bipartite
.. autofunction:: draw_multilayer
.. autofunction:: draw_nodes
.. autofunction:: draw_hyperedges
.. autofunction:: draw_undirected_dyads
.. autofunction:: draw_directed_dyads
.. autofunction:: draw_simplices
.. autofunction:: draw_node_labels
.. autofunction:: draw_hyperedge_labels
1 change: 1 addition & 0 deletions docs/source/api/drawing/xgi.drawing.layout.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ xgi.drawing.layout
.. autofunction:: random_layout
.. autofunction:: pairwise_spring_layout
.. autofunction:: barycenter_spring_layout
.. autofunction:: edge_positions_from_barycenters
.. autofunction:: weighted_barycenter_spring_layout
.. autofunction:: pca_transform
.. autofunction:: circular_layout
Expand Down
4 changes: 2 additions & 2 deletions docs/source/api/recipes/recipes.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -530,8 +530,8 @@
"data": {
"text/plain": [
"(<Axes3DSubplot: >,\n",
" (<mpl_toolkits.mplot3d.art3d.Path3DCollection at 0x28cae3ad0>,\n",
" <mpl_toolkits.mplot3d.art3d.Poly3DCollection at 0x28caaaa50>))"
" (<mpl_toolkits.mplot3d.art3d.Path3DCollection at 0x144a8a690>,\n",
" <mpl_toolkits.mplot3d.art3d.Poly3DCollection at 0x144fe5bd0>))"
]
},
"execution_count": 20,
Expand Down
19 changes: 9 additions & 10 deletions docs/source/api/tutorials/In Depth 2 - Drawing hyperedges.ipynb

Large diffs are not rendered by default.

224 changes: 94 additions & 130 deletions docs/source/api/tutorials/In Depth 3 - Drawing DiHypergraphs.ipynb

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
"import random\n",
"\n",
"import numpy as np\n",
"import pandas as pd\n",
"\n",
"import xgi"
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,6 @@
"source": [
"import random\n",
"\n",
"import numpy as np\n",
"import pandas as pd\n",
"\n",
"import xgi"
]
},
Expand Down
345 changes: 282 additions & 63 deletions docs/source/api/tutorials/Tutorial 5 - Plotting.ipynb

Large diffs are not rendered by default.

42 changes: 27 additions & 15 deletions docs/source/api/tutorials/Tutorial 7 - Directed Hypergraphs.ipynb

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion docs/source/api/tutorials/XGI in 15 minutes.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
"outputs": [],
"source": [
"import matplotlib.pyplot as plt\n",
"import numpy as np\n",
"\n",
"import xgi"
]
Expand Down
1 change: 0 additions & 1 deletion docs/source/api/tutorials/XGI in 5 minutes.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
"outputs": [],
"source": [
"import matplotlib.pyplot as plt\n",
"import numpy as np\n",
"\n",
"import xgi"
]
Expand Down
169 changes: 126 additions & 43 deletions tests/drawing/test_draw.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import numpy as np
import pytest
import seaborn as sb
from matplotlib.patches import FancyArrowPatch

import xgi
from xgi.exception import XGIError
Expand Down Expand Up @@ -519,63 +520,63 @@ def test_draw_multilayer(edgelist8):
plt.close("all")


def test_draw_dihypergraph(diedgelist2, edgelist8):
def test_draw_bipartite(diedgelist2, edgelist8):
DH = xgi.DiHypergraph(diedgelist2)

fig1, ax1 = plt.subplots()
ax1, collections = xgi.draw_dihypergraph(DH, ax=ax1)
node_coll, phantom_node_coll = collections
ax1, collections1 = xgi.draw_bipartite(DH, ax=ax1)
node_coll1, edge_marker_coll1 = collections1
fig2, ax2 = plt.subplots()
ax2, collections2 = xgi.draw_dihypergraph(
ax2, collections2 = xgi.draw_bipartite(
DH,
ax=ax2,
node_fc="red",
node_ec="blue",
node_lw=2,
node_size=20,
lines_fc="blue",
lines_lw=2,
dyad_color="blue",
dyad_lw=2,
edge_marker_fc="red",
edge_marker_lw=2,
edge_marker_size=20,
)
node_coll2, phantom_node_coll2 = collections2
node_coll2, edge_marker_coll2 = collections2

# number of elements
assert len(node_coll.get_offsets()) == 6 # number of original nodes
assert len(phantom_node_coll.get_offsets()) == 3 # number of original edges
assert len(node_coll1.get_offsets()) == 6 # number of original nodes
assert len(edge_marker_coll1.get_offsets()) == 3 # number of original edges
assert len(ax1.patches) == 11 # number of lines

# node face colors
assert np.all(node_coll.get_facecolor() == np.array([[1, 1, 1, 1]])) # white
assert np.all(node_coll1.get_facecolor() == np.array([[1, 1, 1, 1]])) # white
assert np.all(node_coll2.get_facecolor() == np.array([[1, 0, 0, 1]])) # red

# node edge colors
assert np.all(node_coll.get_edgecolor() == np.array([[0, 0, 0, 1]])) # black
assert np.all(node_coll1.get_edgecolor() == np.array([[0, 0, 0, 1]])) # black
assert np.all(node_coll2.get_edgecolor() == np.array([[0, 0, 1, 1]])) # blue

# node_lw
assert np.all(node_coll.get_linewidth() == np.array([1]))
assert np.all(node_coll1.get_linewidth() == np.array([1]))
assert np.all(node_coll2.get_linewidth() == np.array([2]))

# node_size
assert np.all(node_coll.get_sizes() == np.array([7**2]))
assert np.all(node_coll1.get_sizes() == np.array([7**2]))
assert np.all(node_coll2.get_sizes() == np.array([20**2]))

# edge face colors
assert np.all(phantom_node_coll2.get_facecolor() == np.array([[1, 0, 0, 1]])) # red
assert np.all(edge_marker_coll2.get_facecolor() == np.array([[1, 0, 0, 1]])) # red

# edge _lw
assert np.all(phantom_node_coll.get_linewidth() == np.array([1]))
assert np.all(phantom_node_coll2.get_linewidth() == np.array([2]))
assert np.all(edge_marker_coll1.get_linewidth() == np.array([1]))
assert np.all(edge_marker_coll2.get_linewidth() == np.array([2]))

# edge_size
assert np.all(phantom_node_coll.get_sizes() == np.array([7**2]))
assert np.all(phantom_node_coll2.get_sizes() == np.array([20**2]))
assert np.all(edge_marker_coll1.get_sizes() == np.array([7**2]))
assert np.all(edge_marker_coll2.get_sizes() == np.array([20**2]))

# line lw
for patch in ax1.patches: # lines
assert np.all(patch.get_linewidth() == np.array([1.5]))
assert np.all(patch.get_linewidth() == np.array([1]))
for patch in ax2.patches: # lines
assert np.all(patch.get_linewidth() == np.array([2]))

Expand All @@ -584,29 +585,79 @@ def test_draw_dihypergraph(diedgelist2, edgelist8):
assert np.all(patch.get_facecolor() == np.array([[0, 0, 1, 1]]))

# zorder
assert node_coll.get_zorder() == 4
assert phantom_node_coll.get_zorder() == 2
assert node_coll1.get_zorder() == 2
assert edge_marker_coll1.get_zorder() == 1
for patch in ax1.patches: # lines
assert patch.get_zorder() == 0

# test toggle for edges
fig, ax2 = plt.subplots()
ax2, collections = xgi.draw_dihypergraph(DH, edge_marker_toggle=False, ax=ax2)
node_coll, phantom_node_coll = collections
assert len(ax2.collections) == 1
assert phantom_node_coll is None
plt.close("all")

H = xgi.Hypergraph(edgelist8)

fig3, ax3 = plt.subplots()
ax3, collections3 = xgi.draw_bipartite(H, ax=ax3)
node_coll3, edge_marker_coll3, dyad_coll3 = collections3
fig4, ax4 = plt.subplots()
ax4, collections4 = xgi.draw_bipartite(
H,
ax=ax4,
node_fc="red",
node_ec="blue",
node_lw=2,
node_size=20,
dyad_color="blue",
dyad_lw=2,
edge_marker_fc="red",
edge_marker_lw=2,
edge_marker_size=20,
)
node_coll4, edge_marker_coll4, dyad_coll4 = collections4

# number of elements
assert len(node_coll3.get_offsets()) == 7 # number of original nodes
assert len(edge_marker_coll3.get_offsets()) == 9 # number of original edges
assert len(dyad_coll3._paths) == 26 # number of lines

# # node face colors
assert np.all(node_coll3.get_facecolor() == np.array([[1, 1, 1, 1]])) # white
assert np.all(node_coll4.get_facecolor() == np.array([[1, 0, 0, 1]])) # red

# node edge colors
assert np.all(node_coll3.get_edgecolor() == np.array([[0, 0, 0, 1]])) # black
assert np.all(node_coll4.get_edgecolor() == np.array([[0, 0, 1, 1]])) # blue

# # node_lw
assert np.all(node_coll3.get_linewidth() == np.array([1]))
assert np.all(node_coll4.get_linewidth() == np.array([2]))

# # node_size
assert np.all(node_coll3.get_sizes() == np.array([7**2]))
assert np.all(node_coll4.get_sizes() == np.array([20**2]))

# # edge face colors
assert np.all(edge_marker_coll4.get_facecolor() == np.array([[1, 0, 0, 1]])) # red

# # edge _lw
assert np.all(edge_marker_coll3.get_linewidth() == np.array([1]))
assert np.all(edge_marker_coll4.get_linewidth() == np.array([2]))

# # edge_size
assert np.all(edge_marker_coll3.get_sizes() == np.array([7**2]))
assert np.all(edge_marker_coll4.get_sizes() == np.array([20**2]))

plt.close("all")

# test XGI ERROR raise
# test type
with pytest.raises(XGIError):
H = xgi.Hypergraph(edgelist8)
fig, ax3 = plt.subplots()
ax3 = xgi.draw_dihypergraph(H, ax=ax3)
plt.close()
xgi.draw_bipartite([0, 1, 2])

# test gca
fig3, ax = plt.subplots()
ax_gca, collections3 = xgi.draw_bipartite(H)
assert ax == ax_gca


def test_draw_dihypergraph_with_str_labels_and_isolated_nodes():
def test_draw_bipartite_with_str_labels_and_isolated_nodes():
DH1 = xgi.DiHypergraph()
DH1.add_nodes_from(["one", "two", "three", "four", "five", "six"])
DH1.add_edges_from(
Expand All @@ -617,12 +668,46 @@ def test_draw_dihypergraph_with_str_labels_and_isolated_nodes():
)

fig, ax4 = plt.subplots()
ax4, collections4 = xgi.draw_dihypergraph(DH1, ax=ax4)
node_coll4, phantom_node_coll4 = collections4
ax4, collections4 = xgi.draw_bipartite(DH1, ax=ax4)
node_coll4, edge_marker_coll4 = collections4
assert len(node_coll4.get_offsets()) == 6 # number of original nodes
assert len(phantom_node_coll4.get_offsets()) == 2 # number of original edges
assert len(edge_marker_coll4.get_offsets()) == 2 # number of original edges
assert len(ax4.patches) == 7 # number of lines
plt.close()
plt.close("all")


def test_draw_undirected_dyads(edgelist8):
H = xgi.Hypergraph(edgelist8)

fig, ax = plt.subplots()
ax, dyad_collection = xgi.draw_undirected_dyads(H)
assert len(dyad_collection._paths) == 26 # number of lines

with pytest.raises(ValueError):
fig, ax = plt.subplots()
ax, dyad_collection = xgi.draw_undirected_dyads(H, dyad_lw=-1)

fig, ax = plt.subplots()
ax, dyad_collection = xgi.draw_undirected_dyads(
H, dyad_color=np.random.random(H.num_edges)
)
assert len(np.unique(dyad_collection.get_color())) == 28
plt.close("all")


def test_draw_directed_dyads(diedgelist1):
H = xgi.DiHypergraph(diedgelist1)

fig, ax = plt.subplots()
ax = xgi.draw_directed_dyads(H)

with pytest.raises(ValueError):
fig, ax = plt.subplots()
ax = xgi.draw_directed_dyads(H, dyad_lw=-1)

fig, ax = plt.subplots()
ax = xgi.draw_directed_dyads(H, dyad_color=np.random.random(H.num_edges))
plt.close("all")


def test_issue_499(edgelist8):
Expand All @@ -632,11 +717,11 @@ def test_issue_499(edgelist8):

with warnings.catch_warnings():
warnings.simplefilter("error")
ax, collections = xgi.draw(H, ax=ax, node_fc="black")
xgi.draw(H, ax=ax, node_fc="black")

with warnings.catch_warnings():
warnings.simplefilter("error")
ax, collections = xgi.draw(H, ax=ax, node_fc=["black"] * H.num_nodes)
xgi.draw(H, ax=ax, node_fc=["black"] * H.num_nodes)

plt.close("all")

Expand All @@ -646,12 +731,10 @@ def test_issue_515(edgelist8):

with warnings.catch_warnings():
warnings.simplefilter("error")
ax, (node_coll, edge_coll) = xgi.draw_multilayer(H, node_fc="black")
xgi.draw_multilayer(H, node_fc="black")

with warnings.catch_warnings():
warnings.simplefilter("error")
ax, (node_coll, edge_coll) = xgi.draw_multilayer(
H, node_fc=["black"] * H.num_nodes
)
xgi.draw_multilayer(H, node_fc=["black"] * H.num_nodes)

plt.close("all")
Loading

0 comments on commit 46f3be3

Please sign in to comment.