Repository: mne-tools/mne-python

Issue:
Legend colors misbehave on plot_evoked_topo
### Description of the problem

For some reason the procedure I've been using for years to plot multiple evokeds using plot_evoked_topo produces a misbehaving legend.

### Steps to reproduce

```Python
import mne

path = str(mne.datasets.sample.data_path()) + '/MEG/sample/sample_audvis_raw.fif'
raw = mne.io.read_raw_fif(path)

events_1 = mne.find_events(raw)
epochs_1 = mne.Epochs(raw, events_1, event_id={'LH': 1})
evoked_1 = epochs_1.average()

events_2 = mne.find_events(raw)
epochs_2 = mne.Epochs(raw, events_2, event_id={'RH': 2})
evoked_2 = epochs_2.average()

mne.viz.plot_evoked_topo([evoked_1, evoked_2])
```


### Link to data

_No response_

### Expected results

Legend should have two lines that have matching colors to texts.

### Actual results

See:

![image](https://user-images.githubusercontent.com/5773669/196369904-00dfa29d-e570-443d-af83-3755da353951.png)


### Additional information

Platform:         Linux-4.18.0-305.19.1.el8_4.x86_64-x86_64-with-glibc2.35                              
Python:           3.9.13 (main, May 17 2022, 14:19:07)  [GCC 11.3.0]                                    
Executable:       /nix/store/32iyr06f5v32zcglrn4d4jwggy0gslg1-python3-3.9.13/bin/python3                
CPU:              : 8 cores
Memory:           Unavailable (requires "psutil" package)                                               
mne:              1.2.0
numpy:            1.23.1 {unknown linalg bindings}
scipy:            1.8.1
matplotlib:       3.5.2 {backend=QtAgg}

sklearn:          1.1.1
numba:            Not found
nibabel:          Not found
nilearn:          Not found
dipy:             Not found
openmeeg:         Not found
cupy:             Not found
pandas:           Not found
pyvista:          Not found
pyvistaqt:        Not found
ipyvtklink:       Not found
vtk:              Not found
qtpy:             Not found
ipympl:           Not found
pyqtgraph:        Not found
pooch:            v1.6.0

mne_bids:         Not found
mne_nirs:         Not found
mne_features:     Not found
mne_qt_browser:   Not found
mne_connectivity: Not found
mne_icalabel:     Not found



Discussion:
I confirm the problem

> Message ID: ***@***.***>
>

@Teekuningas do you want to try a `git bisect` or looking at the code directly to see where the problem is?
@larsoner I think it's not actually a change in mne, but matplotlib. With matplotlib==3.4.3 it still works and with matplotlib==3.5.0b1 it does not.

The relevant line in mne is here:

https://github.com/mne-tools/mne-python/blob/main/mne/viz/topo.py#L797
Does it work if you do `axes.legend(...)` instead (you might also need a `if not axes: axes = fig.axes[0]` in there)? It should be preferred to `plt.legend` anyway. I wonder if the `add_background_image` now creates a new Axes or something that takes over...
Good try, but still broken with fig.axes[0].legend or with add_background_image commented out.
With no handles given in the legend call, matplotlib lists all the possible handles here:

https://github.com/matplotlib/matplotlib/blob/main/lib/matplotlib/legend.py#L1269

That call to _get_legend_handles returns, in this case, 1221 different elements, starting with:

```
[<matplotlib.collections.PolyCollection object at 0x7fdd4815b160>,
 <matplotlib.lines.Line2D object at 0x7fdd480bd190>,
 <matplotlib.lines.Line2D object at 0x7fdd480bd3d0>,
 <matplotlib.collections.LineCollection object at 0x7fdd480b3eb0>,
 <matplotlib.collections.LineCollection object at 0x7fdd480bd850>,
 <matplotlib.lines.Line2D object at 0x7fdd480bde20>,
 <matplotlib.lines.Line2D object at 0x7fdd480d2130>,
 <matplotlib.collections.LineCollection object at 0x7fdd480bdc10>,
...
]
```

.. out of which the first two are selected due to zip.

And when we use the legend(..) call, it thus uses the PolyCollection and the Line2D objects as handles. And the PolyCollection object in question is probably this one:

https://github.com/mne-tools/mne-python/blob/main/mne/viz/topo.py#L193

Interestingly, this worked for us with matplotlib 3.4.3 because in that version the _get_legend_handles call returned a more sorted list:
```
[<matplotlib.lines.Line2D object at 0x7f8e962f1970>,
 <matplotlib.lines.Line2D object at 0x7f8e962f1c70>,
 <matplotlib.lines.Line2D object at 0x7f8e963076a0>,
 <matplotlib.lines.Line2D object at 0x7f8e96307a30>,
 <matplotlib.lines.Line2D object at 0x7f8e96317460>,
 <matplotlib.lines.Line2D object at 0x7f8e963177f0>,
...
 <matplotlib.lines.Line2D object at 0x7f8e955ce490>,
 <matplotlib.collections.PolyCollection object at 0x7f8e9637dd30>,
 <matplotlib.collections.LineCollection object at 0x7f8e962f16d0>,
...
 <matplotlib.collections.LineCollection object at 0x7f8e955b6dc0>,
 <matplotlib.collections.LineCollection object at 0x7f8e955c3130>,
 <matplotlib.collections.LineCollection object at 0x7f8e955c3b80>,
 <matplotlib.collections.LineCollection object at 0x7f8e955c3eb0>,
 <matplotlib.collections.LineCollection object at 0x7f8e955ce940>]
```

The right fix is probably to explicitly pass the *handles* and the labels instead of just the labels.  That should work with any mpl version. @Teekuningas do you want to give it a shot?
I think so too. Should we just create the handles with something like this?
```
        from matplotlib.lines import Line2D
        handles = [Line2D([0], [0], color=c, lw=1) for c in color]
        legend = plt.legend(
            labels=labels,
            handles=handles,
            loc=legend_loc,
            prop={'size': 10})
```
If we really wanted to stay implicit and lucky, we could also whitelist only Line2D's:
```
        from matplotlib.lines import Line2D
        from matplotlib.legend_handler import HandlerLine2D
        legend = plt.legend(
            labels, 
            loc=legend_loc,
            handler_map={Line2D: HandlerLine2D()},
            prop={'size': 10})
```

To me it's more explicit/safer for whatever internal function is doing the actual plotting to return the `handles` and maybe the `handles, labels` that are appropriate
It's a bit awkward to explicitly collect and pass the handles when we have hundreds of plots to choose from.
I would just take the handles from the first plot
What about this?
```
handles = fig.axes[0].lines[:len(evoked)]
```
That would work, assuming it gives the correct result!

Resolving patch:
diff --git a/mne/viz/topo.py b/mne/viz/topo.py
--- a/mne/viz/topo.py
+++ b/mne/viz/topo.py
@@ -794,8 +794,12 @@ def _plot_evoked_topo(evoked, layout=None, layout_scale=0.945,
     if legend is not False:
         legend_loc = 0 if legend is True else legend
         labels = [e.comment if e.comment else 'Unknown' for e in evoked]
-        legend = plt.legend(labels, loc=legend_loc,
-                            prop={'size': 10})
+        handles = fig.axes[0].lines[:len(evoked)]
+        legend = plt.legend(
+            labels=labels,
+            handles=handles,
+            loc=legend_loc,
+            prop={'size': 10})
         legend.get_frame().set_facecolor(axis_facecolor)
         txts = legend.get_texts()
         for txt, col in zip(txts, color):


Test patch:
diff --git a/mne/viz/tests/test_topo.py b/mne/viz/tests/test_topo.py
--- a/mne/viz/tests/test_topo.py
+++ b/mne/viz/tests/test_topo.py
@@ -135,6 +135,14 @@ def test_plot_topo():
     plot_evoked_topo([evoked, evoked], merge_grads=True,
                      background_color='w', color='blue')
 
+    # test legend colors
+    colors = ['red', 'blue']
+    fig = plot_evoked_topo([evoked, evoked], merge_grads=True, color=colors)
+    legend = fig.axes[0].get_legend()
+    legend_colors = [line.properties()['markeredgecolor']
+                     for line in legend.get_lines()]
+    assert legend_colors == colors
+
     with pytest.raises(ValueError, match='must be .*tuple, list, str,.*'):
         plot_evoked_topo([evoked, evoked], merge_grads=True,
                          color=np.array(["blue", "red"]))