Repository: networkx/networkx

Issue:
Graphviz layout breaks if two nodes have the same string representation
If two nodes in a graph have the same string representation (i.e, `str(u) == str(v)`), computing a graphviz layout is broken. For example:

```
import networkx as nx

class Node(object):
    def __str__(self):
        return 'Foo'

u = Node()
v = Node()
w = Node()

g = nx.DiGraph()
g.add_edges_from([(u,v), (v,w)])

print nx.graphviz_layout(g)
```

This outputs:

```
{
    <__main__.Node object at 0x1045b8dd0>: (0.0, 0.0), 
    <__main__.Node object at 0x1045b8e50>: (0.0, 0.0), 
    <__main__.Node object at 0x1004e2150>: (0.0, 0.0)
}
```

Notice that each of the three nodes is (undesirably) mapped to the same location. If the `__str__` method is removed from the `Node` class definition and the example is run again, we get a more sensible result:

```
{
    <__main__.Node object at 0x1004b4d90>: (-37.382, 60.934), 
    <__main__.Node object at 0x1046c0d50>: (6.6387, 3.6105), 
    <__main__.Node object at 0x1046c0dd0>: (30.743, -64.544)
}
```

The problem can be attributed to the fact that `networkx.graphviz_layout` uses `pydot` to compute the layout. In converting the input graph to a `pydot` graph, the nodes must be converted to strings. This occurs on the [following line](https://github.com/networkx/networkx/blob/master/networkx/drawing/nx_pydot.py#L212) of the `to_pydot` function.

It appears that the issue also occurs when using `networkx.drawing.nx_agraph.pygraphviz_layout` to compute the layout for a similar reason.

The straightforward fix, I think, is to relabel the nodes of the graph given to `networkx.drawing.nx_pydot.pydot_layout` so that they have unique integer indices, then proceed as normal. We must then invert the keys of the resulting layout dictionary before returning it to the user so that they are the original node objects.

Because this must also be done for `networkx.drawing.nx_agraph.pygraphviz_layout`, I think the best fix might be to implement a decorator which performs these transformations, and apply it to both functions. I'd be happy to submit a pull request in the next few days if this sounds like a reasonable approach.



Discussion:
Same issue here.

I am using my custom python class objects instead of strings for nodes.
I believe I have correctly implemented `__hash__`, `__eq__`, `__repr__` and `__str__` methods.
Two objects share the same `__str__` representation (the name that I want to put on the node) but the hash contents are different (eg: in a tree, the parent node is different).

The default set implementation recognizes that the objects are different (based on `__hash__`, `__eq__`) despite having same `__str__`.

So, I guess the node objects are explicitly converted to strings before adding to list/set/dict somewhere.

Trying to fix. Any pointers would be helpful.
Caught the bug.
https://github.com/networkx/networkx/blob/c01040c4f1f50bd036f76a0b26b30d0ad2130565/networkx/drawing/nx_pydot.py#L319
The `make_str()` breaks the functionality for complex objects. 

Suggestion: how about calling `hash(node)` for ID and `str(node)` for name??
Update: 
Now I understand that the pydot.Node doesn't support such functionality. 
It just takes `name` and thats all it does.
Suggestion: extend `pydot.Node` to properly handle name and hashcode 
If somebody is wondering about the issue:
**With pydot**
<img width="952" alt="screen shot 2017-11-04 at 3 42 53 pm" src="https://user-images.githubusercontent.com/1865964/32410145-f16fe25e-c176-11e7-91ae-0964a8e60048.png">

**with kamada_kawai_layout**
<img width="944" alt="screen shot 2017-11-04 at 3 43 21 pm" src="https://user-images.githubusercontent.com/1865964/32410152-1bc7c364-c177-11e7-944c-9df59f987619.png">
The trouble here is representing the nodes with objects that have the same ```str``` representation.
Perhaps the easy fix is to convert nodes to integers, get the layout from that and then convert those positions to the old node names:

    H = nx.convert_node_labels_to_integers(G, label_attribute='node_label')
    H_layout = nx.nx_pydot.pydot_layout(G, prog='dot')
    G_layout = {H.nodes[n]['node_label']: p for n, p in H_layout.items()}

I'll put this into the doc_string as an example usage.

Resolving patch:
diff --git a/networkx/drawing/nx_agraph.py b/networkx/drawing/nx_agraph.py
--- a/networkx/drawing/nx_agraph.py
+++ b/networkx/drawing/nx_agraph.py
@@ -157,7 +157,8 @@ def to_agraph(N):
     # loop over edges
     if N.is_multigraph():
         for u, v, key, edgedata in N.edges(data=True, keys=True):
-            str_edgedata = {k: str(v) for k, v in edgedata.items() if k != 'key'}
+            str_edgedata = {k: str(v) for k, v in edgedata.items()
+                            if k != 'key'}
             A.add_edge(u, v, key=str(key))
             if edgedata is not None:
                 a = A.get_edge(u, v)
@@ -266,6 +267,18 @@ def pygraphviz_layout(G, prog='neato', root=None, args=''):
     >>> pos = nx.nx_agraph.graphviz_layout(G)
     >>> pos = nx.nx_agraph.graphviz_layout(G, prog='dot')
 
+    Notes
+    -----
+    If you use complex node objects, they may have the same string
+    representation and GraphViz could treat them as the same node.
+    The layout may assign both nodes a single location. See Issue #1568
+    If this occurs in your case, consider relabeling the nodes just
+    for the layout computation using something similar to:
+
+        H = nx.convert_node_labels_to_integers(G, label_attribute='node_label')
+        H_layout = nx.nx_agraph.pygraphviz_layout(G, prog='dot')
+        G_layout = {H.nodes[n]['node_label']: p for n, p in H_layout.items()}
+
     """
     try:
         import pygraphviz
diff --git a/networkx/drawing/nx_pydot.py b/networkx/drawing/nx_pydot.py
--- a/networkx/drawing/nx_pydot.py
+++ b/networkx/drawing/nx_pydot.py
@@ -229,7 +229,8 @@ def to_pydot(N):
 
     if N.is_multigraph():
         for u, v, key, edgedata in N.edges(data=True, keys=True):
-            str_edgedata = dict((k, make_str(v)) for k, v in edgedata.items() if k != 'key')
+            str_edgedata = dict((k, make_str(v)) for k, v in edgedata.items()
+                                if k != 'key')
             edge = pydot.Edge(make_str(u), make_str(v),
                               key=make_str(key), **str_edgedata)
             P.add_edge(edge)
@@ -261,8 +262,8 @@ def graphviz_layout(G, prog='neato', root=None, **kwds):
 
 
 # FIXME: Document the "root" parameter.
-# FIXME: Why does this function accept a variadic dictionary of keyword arguments
-# (i.e., "**kwds") but fail to do anything with those arguments? This is probably
+# FIXME: Why does this function accept a variadic dict of keyword arguments
+# (i.e., "**kwds") but fail to do anything with them? This is probably
 # wrong, as unrecognized keyword arguments will be silently ignored.
 def pydot_layout(G, prog='neato', root=None, **kwds):
     """Create node positions using :mod:`pydot` and Graphviz.
@@ -273,7 +274,7 @@ def pydot_layout(G, prog='neato', root=None, **kwds):
         NetworkX graph to be laid out.
     prog : optional[str]
         Basename of the GraphViz command with which to layout this graph.
-        Defaults to `neato`, the default GraphViz command for undirected graphs.
+        Defaults to `neato`: default GraphViz command for undirected graphs.
 
     Returns
     --------
@@ -285,6 +286,19 @@ def pydot_layout(G, prog='neato', root=None, **kwds):
     >>> G = nx.complete_graph(4)
     >>> pos = nx.nx_pydot.pydot_layout(G)
     >>> pos = nx.nx_pydot.pydot_layout(G, prog='dot')
+
+    Notes
+    -----
+    If you use complex node objects, they may have the same string
+    representation and GraphViz could treat them as the same node.
+    The layout may assign both nodes a single location. See Issue #1568
+    If this occurs in your case, consider relabeling the nodes just
+    for the layout computation using something similar to:
+
+        H = nx.convert_node_labels_to_integers(G, label_attribute='node_label')
+        H_layout = nx.nx_pydot.pydot_layout(G, prog='dot')
+        G_layout = {H.nodes[n]['node_label']: p for n, p in H_layout.items()}
+
     """
     pydot = _import_pydot()
     P = to_pydot(G)
@@ -295,7 +309,7 @@ def pydot_layout(G, prog='neato', root=None, **kwds):
     # from the passed graph with the passed external GraphViz command.
     D_bytes = P.create_dot(prog=prog)
 
-    # Unique string decoded from these bytes with the preferred locale encoding.
+    # Unique string decoded from these bytes with the preferred locale encoding
     D = unicode(D_bytes, encoding=getpreferredencoding())
 
     if D == "":  # no data returned


Test patch: