Repository: networkx/networkx

Issue:
Ordering of layers in multipartite_layout changed in v2.7. Add a way to control the order?
I am trying to run a script to plot graphs, but they're coming out wrong. I've tracked down the issue to incorrect positions obtained from the multipartite_layout command, which is either ignoring or misordering the node layer information.

### Current Behavior
The positions output by multipartite_layout don't order the layers correctly.

### Expected Behavior
The positions output by multipartite_layout should order the layers by value.

### Steps to Reproduce
Here is a snippet of what I currently get for the position of nodes in a small graph:
import networkx as nx

```
G = nx.DiGraph()
G.add_node('a',layer=2)
G.add_node('b',layer=3)
G.add_node('c',layer=1)
G.add_node('d',layer=2)
G.add_node('e',layer=4)
nx.multipartite_layout(G,align='horizontal',subset_key='layer')

{'d': array([-0.27777778, -0.66666667]),
 'a': array([ 0.27777778, -0.66666667]),
 'b': array([ 0.        , -0.11111111]),
 'c': array([0.        , 0.44444444]),
 'e': array([0., 1.])}
```
What I expect to get out (and can still see if I use an online compiler) is
```
{'d': array([-0.3125, -0.25  ]),
 'a': array([ 0.3125, -0.25  ]),
 'b': array([0.   , 0.375]),
 'c': array([ 0.   , -0.875]),
 'e': array([0., 1.])}
```


### Environment
Python version: 3.9.12
NetworkX version: 2.7.1


### Additional context
On an older computer when I had this issue I was able to resolve it by uninstalling and reinstalling Anaconda. This time I'm working from a new computer with a fresh install and uninstalling/reinstalling doesn't work.



Discussion:
In PR #5153 we allowed non-numeric layer values. But that change also impacts the order of the layers in the multipartite layout. The order of the layers is no longer determined by the layer's numeric value.  Notice that we never intended the layer's value to be required to be a number nor to reflect the position/order of the layers. But I can see why it would be surprising to suddenly have that change.

At the moment, the order of the layers is determined by the order that the nodes are reported by `G.nodes`.  That is, the layers are ordered based on the first time in `G.nodes` that a node has that layer value.  So, with your example, the nodes are ordered alphabetically, so the layer for 'a' is the first layer even though it's layer is `2`. Layer `1` is not positioned until node `c` is processed.  

Technically this is not a bug because we never guaranteed the order of the layers, but our implementation did provide an order and we changed that order. So we broke backward compatibility and should provide a way to indicate the order of the layers.

I'm going to change the title of this issue to reflect a request for the ability to choose the order of the layers in a multipartitie layout.  A solution will need a way to allow the user to indicate the order of the layers while still allowing non-numeric layer values.  Suggestions?
Good to know how the layers are assigned. Now that I know how it works I'm working around it, for now, by creating a duplicate graph where nodes are added in the desired order. Part of me wants to think that when the layers are indicated with integers it should still default to that order (rather than input order), though maybe a choice can be made between "input" or "sorted."
Perhaps we could do something like this:

```python
try:
    layers = sorted((lyr, i, nlist) for i, (lyr, nlist) in enumerate(layers.items()))
except TypeError:
    layers = [(lyr, i, nlist) for i, (lyr, nlist) in enumerate(layers.items())]
for lyr, i, nlist in layers:
```

This would handle the case when the layer identifiers are unsortable, but it will also work for ordering the layers according to a sort of the layer identifier when all layer identifiers are sortable.  It does **not** allow the layer identifier to provide the numerical value for the position of that layer.  Is that important to allow? That is how it used to work, so maybe good to have it available...
    
> This would handle the case when the layer identifiers are unsortable, but it will also work for ordering the layers according to a sort of the layer identifier when all layer identifiers are sortable. It does not allow the layer identifier to provide the numerical value for the position of that layer. Is that important to allow? That is how it used to work, so maybe good to have it available...

I think this is the way to go. Even though the ordering was never guaranteed, it's clear that users are relying on it (see also #5515 ) and it's perfectly reasonable to expect numeric layers to be sorted. I prefer the solution proposed above to e.g. adding a new keyword argument. 

Resolving patch:
diff --git a/networkx/drawing/layout.py b/networkx/drawing/layout.py
--- a/networkx/drawing/layout.py
+++ b/networkx/drawing/layout.py
@@ -1083,11 +1083,16 @@ def multipartite_layout(G, subset_key="subset", align="vertical", scale=1, cente
             raise ValueError(msg)
         layers[layer] = [v] + layers.get(layer, [])
 
+    # Sort by layer, if possible
+    try:
+        layers = sorted(layers.items())
+    except TypeError:
+        layers = list(layers.items())
+
     pos = None
     nodes = []
-
     width = len(layers)
-    for i, layer in enumerate(layers.values()):
+    for i, (_, layer) in enumerate(layers):
         height = len(layer)
         xs = np.repeat(i, height)
         ys = np.arange(0, height, dtype=float)


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
@@ -422,3 +422,24 @@ def test_multipartite_layout_nonnumeric_partition_labels():
     G.add_edges_from([(0, 2), (0, 3), (1, 2)])
     pos = nx.multipartite_layout(G)
     assert len(pos) == len(G)
+
+
+def test_multipartite_layout_layer_order():
+    """Return the layers in sorted order if the layers of the multipartite
+    graph are sortable. See gh-5691"""
+    G = nx.Graph()
+    for node, layer in zip(("a", "b", "c", "d", "e"), (2, 3, 1, 2, 4)):
+        G.add_node(node, subset=layer)
+
+    # Horizontal alignment, therefore y-coord determines layers
+    pos = nx.multipartite_layout(G, align="horizontal")
+
+    # Nodes "a" and "d" are in the same layer
+    assert pos["a"][-1] == pos["d"][-1]
+    # positions should be sorted according to layer
+    assert pos["c"][-1] < pos["a"][-1] < pos["b"][-1] < pos["e"][-1]
+
+    # Make sure that multipartite_layout still works when layers are not sortable
+    G.nodes["a"]["subset"] = "layer_0"  # Can't sort mixed strs/ints
+    pos_nosort = nx.multipartite_layout(G)  # smoke test: this should not raise
+    assert pos_nosort.keys() == pos.keys()