Repository: networkx/networkx

Issue:
Shell Layout incorrectly rescales for single element shells.
Hi there!

I had a shell layout that used to work with the 1.1 release, but incorrectly overlaps nodes when there is only one element in the shell in the latest build.

Upon inspection, I believe the `rescale_layout` function to be the culprit.
https://github.com/networkx/networkx/blob/794323b9ca4fcd5bb270137ba2fe51bcb78b9a6f/networkx/drawing/layout.py#L252

For a two shell graph with one element in the outer shell: `nlist [[1], [2]]`, `shell_layout` outputs:
`{1: array([0., 0.]), 2: array([0.0, 0. ])}`
where it used to output:
`{1: array([0., 0.]), 2: array([1.0, 0. ])}`

For a two shell graph with two element in the outer shell: `nlist [[1], [2, 3]]`, `shell_layout` correctly outputs:
```
{1: array([0., 0.]),
 2: array([5.00000000e-01, 2.18556941e-08]),
 3: array([-5.00000000e-01, -2.18556941e-08])}
```

We see in the `rescale_layout`, that the positions are being normalized. Unfortunately this means for single node shells, they shift to zero as it's position value is also the mean.
https://github.com/networkx/networkx/blob/794323b9ca4fcd5bb270137ba2fe51bcb78b9a6f/networkx/drawing/layout.py#L889

I believe a simple solution is to first check that there is more than one element:
```python
if len(pos[:,i]) > 1:
            pos[:, i] -= pos[:, i].mean()
```

This results in an acceptable set of positions, while not changing any instances for shells with more nodes:
`{1: array([0., 0.]), 2: array([0.5, 0. ])}`

I'd be happy to submit a PR, once I check to ensure no other issues with the other layouts.


Discussion:
I agree with you that there is a bug in how the code rescales the shell layouts. If there is a single node in the shell, it moves that node to the middle of the diagram.

I would like to fix the behavior by changing the ```shell_layout``` function rather than touching ```rescale_layout``` though. There are a lot of functions that use rescale_layout and they might not work as expected if we don't shift one node layouts to the origin. Instead I am hoping that a similar ```if``` statement in the ```shell_layout``` could determine whether rescale_layout gets used at all. If there is one node, then set that coordinate appropriately and otherwise call the rescale function.  Do you see any downside to this approach?
### Problem
I believe if we tried to delegate that responsibility of a binary rescale vs no-rescale to `shell_layout` we'd run into issues with partial rescaling causing distorted graphs.
For example lets say in shell_layout we tried to rescale only if there were more than one node in a shell:
```python
if len(node) > 1:
    pos = rescale_layout(pos, scale=scale * radius / len(nlist)) + center 
```
Then a call to `shell_layout(nlist=[[1],[2,3],[4]])` would result in:
```python
{1: array([1., 0.], dtype=float32),
 2: array([3.33333343e-01, 1.45704631e-08]),
 3: array([-3.33333343e-01, -1.45704631e-08]),
 4: array([1., 0.], dtype=float32)}
```

### Alternative:
What if we modified the `rescale_layout` such that normalization was optional by means of a default parameter `norm=True`. This would allow the caller to decide the conditions under which the rescaling should include normalization and by using a default argument which encapsulates the current functionality, would not break any other functions.

So `rescale_layout` would become:
```python
def rescale_layout(pos, scale=1, norm=True):
    """
    Parameters
    ----------
    ...
    norm : boolean (default: True)
        normalize positions as part of rescaling.
    ...
    """
    for i in range(pos.shape[1]):
        if norm:
            pos[:, i] -= pos[:, i].mean()
        lim = max(abs(pos[:, i]).max(), lim)
````

Then we could leave it up to the caller on when to rescale.
Therefore `shell_layout` would elect to only normalize when there was more than one node in the shell:
```python
   norm = len(nodes) > 1
   pos = rescale_layout(pos, scale=scale * radius / len(nlist), norm=norm) + center
```

```python
shell_layout2(nlist=[[1],[2,3],[4],[5,6],[7,8,9,10],[11]])

{1: array([0., 0.]),
 2: array([1.66666672e-01, 7.28523153e-09]),
 3: array([-1.66666672e-01, -7.28523153e-09]),
 4: array([0.33333334, 0.        ]),
 5: array([5.00000000e-01, 2.18556941e-08]),
 6: array([-5.00000000e-01, -2.18556941e-08]),
 7: array([6.66666687e-01, 9.93410776e-09]),
 8: array([-2.11942996e-08,  6.66666687e-01]),
 9: array([-6.66666687e-01, -4.83477436e-08]),
 10: array([ 1.58965481e-08, -6.66666687e-01]),
 11: array([0.83333331, 0.        ])}
```
I'm suggesting that we *do* scale when there is only one node in a shell. Just not using the ```rescale_layout``` code. Of course, the outer shells have a different radius than inner shells, so we have to scale each shell.  It should be something like:

    if len(node) > 1:
        pos = rescale_layout(pos, scale=scale * radius / len(nlist)) + center 
    else:
        pos = [(scale * radius + center[0], center[1])]    # or something similar

While my own coding ideology prefers to modify `rescale_layout` to accommodate optionally scaling without shifting the mean, and thereby consolidate all scaling operations, I completely understand the rationale for leaving it untouched (particularly when an `if-else` will suffice).

`pos = [(scale * radius + center[0], center[1])]` works great.

I'd be happy to pull a PR if you'd like.
A PR would be really helpful.  Thank you for being flexible.
On a stylistic note, is it also OK in the same PR to modify (I know some repos prefer those type of changes separate):
https://github.com/networkx/networkx/blob/794323b9ca4fcd5bb270137ba2fe51bcb78b9a6f/networkx/drawing/layout.py#L805
to `pos = rescale_layout(pos, scale=scale) + center`

to match the other invocations:
https://github.com/networkx/networkx/blob/794323b9ca4fcd5bb270137ba2fe51bcb78b9a6f/networkx/drawing/layout.py#L174
https://github.com/networkx/networkx/blob/794323b9ca4fcd5bb270137ba2fe51bcb78b9a6f/networkx/drawing/layout.py#L252
https://github.com/networkx/networkx/blob/794323b9ca4fcd5bb270137ba2fe51bcb78b9a6f/networkx/drawing/layout.py#L326
https://github.com/networkx/networkx/blob/794323b9ca4fcd5bb270137ba2fe51bcb78b9a6f/networkx/drawing/layout.py#L340
https://github.com/networkx/networkx/blob/794323b9ca4fcd5bb270137ba2fe51bcb78b9a6f/networkx/drawing/layout.py#L469
https://github.com/networkx/networkx/blob/794323b9ca4fcd5bb270137ba2fe51bcb78b9a6f/networkx/drawing/layout.py#L688
I remember we had some discussions about the `rescale_layout` function when I introduced a layout for bipartite networks. You might find #2976 and #2986 helpful.
Yes -- ```scale=scale``` is better for line 805.

Resolving patch:
diff --git a/networkx/drawing/layout.py b/networkx/drawing/layout.py
--- a/networkx/drawing/layout.py
+++ b/networkx/drawing/layout.py
@@ -252,7 +252,10 @@ def shell_layout(G, nlist=None, scale=1, center=None, dim=2):
         theta = np.linspace(0, 1, len(nodes) + 1)[:-1] * 2 * np.pi
         theta = theta.astype(np.float32)
         pos = np.column_stack([np.cos(theta), np.sin(theta)])
-        pos = rescale_layout(pos, scale=scale * radius / len(nlist)) + center
+        if len(pos) > 1:
+            pos = rescale_layout(pos, scale=scale * radius / len(nlist)) + center
+        else:
+            pos = np.array([(scale * radius + center[0], center[1])])
         npos.update(zip(nodes, pos))
         radius += 1.0
 
@@ -833,7 +836,7 @@ def spectral_layout(G, weight='weight', scale=1, center=None, dim=2):
             A += A.T
         pos = _spectral(A, dim)
 
-    pos = rescale_layout(pos, scale) + center
+    pos = rescale_layout(pos, scale=scale) + center
     pos = dict(zip(G, pos))
     return pos
 


Test patch:
diff --git a/networkx/drawing/tests/test_layout.py b/networkx/drawing/tests/test_layout.py
--- a/networkx/drawing/tests/test_layout.py
+++ b/networkx/drawing/tests/test_layout.py
@@ -1,7 +1,7 @@
 """Unit tests for layout functions."""
 from nose import SkipTest
 from nose.tools import assert_almost_equal, assert_equal, \
-    assert_false, assert_raises
+    assert_true, assert_false, assert_raises
 import networkx as nx
 
 
@@ -25,7 +25,7 @@ def setUp(self):
         self.Gi = nx.grid_2d_graph(5, 5)
         self.Gs = nx.Graph()
         nx.add_path(self.Gs, 'abcdef')
-        self.bigG = nx.grid_2d_graph(25, 25)  # bigger than 500 nodes for sparse
+        self.bigG = nx.grid_2d_graph(25, 25)  # > 500 nodes for sparse
 
     @staticmethod
     def collect_node_distances(positions):
@@ -183,9 +183,10 @@ def test_single_nodes(self):
         G = nx.path_graph(1)
         vpos = nx.shell_layout(G)
         assert_false(vpos[0].any())
-        G = nx.path_graph(3)
-        vpos = nx.shell_layout(G, [[0], [1, 2]])
+        G = nx.path_graph(4)
+        vpos = nx.shell_layout(G, [[0], [1, 2], [3]])
         assert_false(vpos[0].any())
+        assert_true(vpos[3].any())  # ensure node 3 not at origin (#3188)
 
     def test_smoke_initial_pos_fruchterman_reingold(self):
         pos = nx.circular_layout(self.Gi)
@@ -194,11 +195,11 @@ def test_smoke_initial_pos_fruchterman_reingold(self):
     def test_fixed_node_fruchterman_reingold(self):
         # Dense version (numpy based)
         pos = nx.circular_layout(self.Gi)
-        npos = nx.fruchterman_reingold_layout(self.Gi, pos=pos, fixed=[(0, 0)])
+        npos = nx.spring_layout(self.Gi, pos=pos, fixed=[(0, 0)])
         assert_equal(tuple(pos[(0, 0)]), tuple(npos[(0, 0)]))
         # Sparse version (scipy based)
         pos = nx.circular_layout(self.bigG)
-        npos = nx.fruchterman_reingold_layout(self.bigG, pos=pos, fixed=[(0, 0)])
+        npos = nx.spring_layout(self.bigG, pos=pos, fixed=[(0, 0)])
         for axis in range(2):
             assert_almost_equal(pos[(0, 0)][axis], npos[(0, 0)][axis])
 
@@ -222,12 +223,12 @@ def test_center_parameter(self):
 
     def test_center_wrong_dimensions(self):
         G = nx.path_graph(1)
+        assert_equal(id(nx.spring_layout), id(nx.fruchterman_reingold_layout))
         assert_raises(ValueError, nx.random_layout, G, center=(1, 1, 1))
         assert_raises(ValueError, nx.circular_layout, G, center=(1, 1, 1))
         assert_raises(ValueError, nx.planar_layout, G, center=(1, 1, 1))
         assert_raises(ValueError, nx.spring_layout, G, center=(1, 1, 1))
-        assert_raises(ValueError, nx.fruchterman_reingold_layout, G, center=(1, 1, 1))
-        assert_raises(ValueError, nx.fruchterman_reingold_layout, G, dim=3, center=(1, 1))
+        assert_raises(ValueError, nx.spring_layout, G, dim=3, center=(1, 1))
         assert_raises(ValueError, nx.spectral_layout, G, center=(1, 1, 1))
         assert_raises(ValueError, nx.spectral_layout, G, dim=3, center=(1, 1))
         assert_raises(ValueError, nx.shell_layout, G, center=(1, 1, 1))
@@ -255,7 +256,7 @@ def test_empty_graph(self):
         assert_equal(vpos, {})
 
     def test_bipartite_layout(self):
-        G = nx.complete_bipartite_graph(3,5)
+        G = nx.complete_bipartite_graph(3, 5)
         top, bottom = nx.bipartite.sets(G)
 
         vpos = nx.bipartite_layout(G, top)
@@ -313,7 +314,8 @@ def test_kamada_kawai_costfn_2d(self):
         expected_cost = 0.5 * meanwt * numpy.sum(numpy.sum(pos, axis=0) ** 2)
         for i in range(pos.shape[0]):
             for j in range(i + 1, pos.shape[0]):
-                expected_cost += (numpy.linalg.norm(pos[i] - pos[j]) * invdist[i][j] - 1.0) ** 2
+                diff = numpy.linalg.norm(pos[i] - pos[j])
+                expected_cost += (diff * invdist[i][j] - 1.0) ** 2
 
         assert_almost_equal(cost, expected_cost)