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 visualization to retworkx for networkx users guide #344

Merged
merged 5 commits into from
May 29, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,5 @@ retworkx/*pyd
*.stestr/
*.ps
*.png
*.svg
*.jpg
2 changes: 2 additions & 0 deletions docs/source/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,8 @@ type functions in the algorithms API but can be run with a
retworkx.random_layout
retworkx.spring_layout

.. _layout-functions:

Layout Functions
================

Expand Down
64 changes: 64 additions & 0 deletions docs/source/networkx.rst
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,70 @@ This difference with retworkx is primarily because numpy exposes a public C
interface which retworkx can interface with directly, while the other
libraries and types only expose Python APIs.

Visualization Functions
-----------------------

NetworkX provides a native drawer with a matplotlib drawer (the
``networkx_drawer*`` functions) and then functions to interface with
``pygraphviz`` and ``pydot`` to enable visualization with graphviz via those
libraries (in addition to functions to serialize graphs in formats other
graph visualization tools can use). NetworkX also provides several functions
`layout functions <https://networkx.org/documentation/stable/reference/drawing.html#module-networkx.drawing.layout>`__
for generating different layouts that can be used for visualizing the graph.


retworkx has drawer functions with 2 visualization backends, matplotlib
(:func:`~retworkx.visualization.mpl_draw`) and graphviz
(:func:`~retworkx.visualization.graphviz_draw`). Unlike networkx the
:func:`~retworkx.visualization.graphviz_draw` will handle calling graphviz and
generate an image file. For layout functions retworkx has a similar variety of
:ref:`layout-functions`, however it should be noted that retworkx's functions
are strictly 2 dimensional. The also return a :class:`~retworkx.Pos2DMapping`
custom return type which acts as read-only dictionary (which is different from
networkx which returns a normal dictionary that can be modified).

Matplotlib Drawers
^^^^^^^^^^^^^^^^^^

The retwork function :func:`~retworkx.visualization.mpl_draw` function is
basically equivalent to the networkx function ``draw_networkx`` (it was
actually originally forked from the networkx drawer). However, there are some
key differences to keep in mind between the networkx and retworkx matplotlib
drawer.

``networkx.draw_networkx`` and ``retworkx.mpl_draw`` differences:

.. list-table::
:header-rows: 1

* - networkx
- retworkx
- Notes
* - ``nodelist``
- ``node_list``
-
* - ``edgelist``
- ``edge_list``
-
* - ``arrowsize``
- ``arrow_size``
-
* - ``labels``
- ``labels``
- For ``networkx_drawer`` ``labels`` is a dict of nodes to their label,
while retworkx's ``mpl_drawer`` ``labels`` is a callback function
that will be passed a node's data payload and expected to return the
node's label
* - ``networkx.draw_networkx_edge_labels()``
- ``edge_labels``
- NetworkX's ``networkx_drawer`` doesn't have an option for edge labels
and instead adding labels is only exposed via a separate function
``draw_networkx_edge_labels()`` which requires the ``pos`` dictionary
from the original visualization to be used. retworkx's ``edge_labels``
kwarg takes a callback function that will be passed an edge's data
payload and expected to return the label.


.. _networkx_converter:

Converting from a networkx graph
Expand Down
2 changes: 1 addition & 1 deletion docs/source/visualization.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ Visualization API
:toctree: stubs

retworkx.visualization.mpl_draw
retworkx.visualization.pydot_draw
retworkx.visualization.graphviz_draw
6 changes: 3 additions & 3 deletions releasenotes/notes/pydot-drawer-c5df0aa830679748.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
features:
- |
Added a new `Graphviz <https://graphviz.org/>`__ based drawer function,
:func:`~retworkx.visualization.pydot_draw`, to the
:func:`~retworkx.visualization.graphviz_draw`, to the
:mod:`retworkx.visualization` module. This function requires that
Graphviz is installed locally and adds two new optional dependencies,
`pydot <https://pypi.org/project/pydot/>`__ which is used to call Graphviz
Expand All @@ -15,7 +15,7 @@ features:
.. jupyter-execute::

import retworkx
from retworkx.visualization import pydot_draw
from retworkx.visualization import graphviz_draw

def node_attr(node):
if node == 0:
Expand All @@ -26,4 +26,4 @@ features:
return {'color': 'red', 'fillcolor': 'red', 'style': 'filled'}

graph = retworkx.generators.directed_star_graph(weights=list(range(32)))
pydot_draw(graph, node_attr_fn=node_attr, method='sfdp')
graphviz_draw(graph, node_attr_fn=node_attr, method='sfdp')
4 changes: 2 additions & 2 deletions retworkx/visualization/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@

__all__ = [
"mpl_draw",
"pydot_draw",
"graphviz_draw",
]

from .matplotlib import mpl_draw
from .pydot import pydot_draw
from .graphviz import graphviz_draw
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@
except ImportError:
HAS_PYDOT = False

__all__ = ["pydot_draw"]
__all__ = ["graphviz_draw"]


def pydot_draw(
def graphviz_draw(
graph,
node_attr_fn=None,
edge_attr_fn=None,
Expand All @@ -30,7 +30,7 @@ def pydot_draw(
method=None,
):
"""Draw a :class:`~retworkx.PyGraph` or :class:`~retworkx.PyDiGraph` object
using graphviz via pydot
using graphviz
.. note::
Expand Down Expand Up @@ -87,7 +87,7 @@ def pydot_draw(
"""
if not HAS_PYDOT:
raise ImportError(
"Pydot and Pillow are necessary to use pydot_draw() "
"Pydot and Pillow are necessary to use graphviz_draw() "
"it can be installed with 'pip install pydot pillow'"
)
try:
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,6 @@ def readme():
install_requires=['numpy>=1.16.0'],
extras_require={
'mpl': ['matplotlib>=3.0'],
'pydot': ['pydot>=1.4', 'pillow>=5.4'],
'graphviz': ['pydot>=1.4', 'pillow>=5.4'],
}
)
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import unittest

import retworkx
from retworkx.visualization import pydot_draw
from retworkx.visualization import graphviz_draw

try:
import pydot
Expand All @@ -37,12 +37,12 @@ def _save_image(image, path):
@unittest.skipUnless(
HAS_PYDOT, "pydot and graphviz are required for running these tests"
)
class TestPyDotDraw(unittest.TestCase):
class TestGraphvizDraw(unittest.TestCase):
def test_draw_no_args(self):
graph = retworkx.generators.star_graph(24)
image = pydot_draw(graph)
image = graphviz_draw(graph)
self.assertIsInstance(image, PIL.Image.Image)
_save_image(image, "test_pydot_draw.png")
_save_image(image, "test_graphviz_draw.png")

def test_draw_node_attr_fn(self):
graph = retworkx.PyGraph()
Expand All @@ -63,9 +63,9 @@ def test_draw_node_attr_fn(self):
}
)
graph.add_edge(0, 1, dict(label="1", name="1"))
image = pydot_draw(graph, lambda node: node)
image = graphviz_draw(graph, lambda node: node)
self.assertIsInstance(image, PIL.Image.Image)
_save_image(image, "test_pydot_draw_node_attr.png")
_save_image(image, "test_graphviz_draw_node_attr.png")

def test_draw_edge_attr_fn(self):
graph = retworkx.PyGraph()
Expand All @@ -86,9 +86,9 @@ def test_draw_edge_attr_fn(self):
}
)
graph.add_edge(0, 1, dict(label="1", name="1"))
image = pydot_draw(graph, lambda node: node, lambda edge: edge)
image = graphviz_draw(graph, lambda node: node, lambda edge: edge)
self.assertIsInstance(image, PIL.Image.Image)
_save_image(image, "test_pydot_draw_edge_attr.png")
_save_image(image, "test_graphviz_draw_edge_attr.png")

def test_draw_graph_attr(self):
graph = retworkx.PyGraph()
Expand All @@ -110,32 +110,32 @@ def test_draw_graph_attr(self):
)
graph.add_edge(0, 1, dict(label="1", name="1"))
graph_attr = {"bgcolor": "red"}
image = pydot_draw(
image = graphviz_draw(
graph, lambda node: node, lambda edge: edge, graph_attr
)
self.assertIsInstance(image, PIL.Image.Image)
_save_image(image, "test_pydot_draw_graph_attr.png")
_save_image(image, "test_graphviz_draw_graph_attr.png")

def test_image_type(self):
graph = retworkx.directed_gnp_random_graph(50, 0.8)
image = pydot_draw(graph, image_type="jpg")
image = graphviz_draw(graph, image_type="jpg")
self.assertIsInstance(image, PIL.Image.Image)
_save_image(image, "test_pydot_draw_image_type.jpg")
_save_image(image, "test_graphviz_draw_image_type.jpg")

def test_method(self):
graph = retworkx.directed_gnp_random_graph(50, 0.8)
image = pydot_draw(graph, method="sfdp")
image = graphviz_draw(graph, method="sfdp")
self.assertIsInstance(image, PIL.Image.Image)
_save_image(image, "test_pydot_method.png")
_save_image(image, "test_graphviz_method.png")

def test_filename(self):
graph = retworkx.generators.grid_graph(20, 20)
pydot_draw(
graphviz_draw(
graph,
filename="test_pydot_filename.svg",
filename="test_graphviz_filename.svg",
image_type="svg",
method="neato",
)
self.assertTrue(os.path.isfile("test_pydot_filename.svg"))
self.assertTrue(os.path.isfile("test_graphviz_filename.svg"))
if not SAVE_IMAGES:
self.addCleanup(os.remove, "test_pydot_filename.svg")
self.addCleanup(os.remove, "test_graphviz_filename.svg")