Repository: mne-tools/mne-python

Issue:
plot_compare_evokeds only plots one waveform, given list of inputs and identical comments
Between v 0.20.7 and 0.21.2, the way mnw.viz.plot_compare_evokeds() deals with a list of inputs haschanged. 

#### Steps to reproduce
Given a dict of Evokeds such as this:

```
gavg = {'Condition A': <Evoked | 'Grand average (n = 20)' (average, N=20), [-0.19922, 1] sec, 64 ch, ~479 kB>,
        'Condition B': <Evoked | 'Grand average (n = 20)' (average, N=20), [-0.19922, 1] sec, 64 ch, ~479 kB>,
        'Condition C': <Evoked | 'Grand average (n = 20)' (average, N=20), [-0.19922, 1] sec, 64 ch, ~479 kB>,
        'Condition D': <Evoked | 'Grand average (n = 20)' (average, N=20), [-0.19922, 1] sec, 64 ch, ~479 kB>}
```

and the command:

`mne.viz.plot_compare_evokeds([gavg['Condition A'], gavg['Condition B']]`

...the previous (0.20.7) behavior was to plot a line for each input in the list.

The new (0.21.2) behavior plots a single line. 

The documentation for each version is below, with the change in bold:

### 0.20.7
> If a single Evoked instance, it is plotted as a time series. If a dict whose values are Evoked objects, the contents are plotted as single time series each and the keys are used as condition labels. If a list of Evokeds, the contents are plotted with indices as labels. If a [dict/list] of lists, the unweighted mean is plotted as a time series and the parametric confidence interval is plotted as a shaded area. All instances must have the same shape - channel numbers, time points etc. If dict, keys must be of type str.

### 0.21.2
> If a single Evoked instance, it is plotted as a time series. **If a list of Evokeds, the contents are plotted with their .comment attributes used as condition labels. If no comment is set, the index of the respectiv Evoked the list will be used instead, starting with 1 for the first Evoked.** If a dict whose values are Evoked objects, the contents are plotted as single time series each and the keys are used as labels. If a [dict/list] of lists, the unweighted mean is plotted as a time series and the parametric confidence interval is plotted as a shaded area. All instances must have the same shape - channel numbers, time points etc. If dict, keys must be of type str.

What does not seem to be documented (and, IMHO, is a bug), is that if multiple inputs have the same comment, only the last input is plotted. Perhaps in this case all inputs could be plotted, but a warning could be issued about the identical comments?

Note as well the typo 'respectiv' in the newer version of the documentation.



Discussion:

Resolving patch:
diff --git a/mne/viz/evoked.py b/mne/viz/evoked.py
--- a/mne/viz/evoked.py
+++ b/mne/viz/evoked.py
@@ -1933,7 +1933,7 @@ def plot_compare_evokeds(evokeds, picks=None, colors=None,
         If a single Evoked instance, it is plotted as a time series.
         If a list of Evokeds, the contents are plotted with their
         ``.comment`` attributes used as condition labels. If no comment is set,
-        the index of the respectiv Evoked the list will be used instead,
+        the index of the respective Evoked the list will be used instead,
         starting with ``1`` for the first Evoked.
         If a dict whose values are Evoked objects, the contents are plotted as
         single time series each and the keys are used as labels.
@@ -2141,13 +2141,15 @@ def plot_compare_evokeds(evokeds, picks=None, colors=None,
     if isinstance(evokeds, (list, tuple)):
         evokeds_copy = evokeds.copy()
         evokeds = dict()
-
-        for evk_idx, evk in enumerate(evokeds_copy, start=1):
-            label = None
-            if hasattr(evk, 'comment'):
-                label = evk.comment
-            label = label if label else str(evk_idx)
-            evokeds[label] = evk
+        comments = [getattr(_evk, 'comment', None) for _evk in evokeds_copy]
+        for idx, (comment, _evoked) in enumerate(zip(comments, evokeds_copy)):
+            key = str(idx + 1)
+            if comment:  # only update key if comment is non-empty
+                if comments.count(comment) == 1:  # comment is unique
+                    key = comment
+                else:  # comment is non-unique: prepend index
+                    key = f'{key}: {comment}'
+            evokeds[key] = _evoked
         del evokeds_copy
 
     if not isinstance(evokeds, dict):
diff --git a/tutorials/evoked/plot_20_visualize_evoked.py b/tutorials/evoked/plot_20_visualize_evoked.py
--- a/tutorials/evoked/plot_20_visualize_evoked.py
+++ b/tutorials/evoked/plot_20_visualize_evoked.py
@@ -5,7 +5,7 @@
 =======================
 
 This tutorial shows the different visualization methods for
-:class:`~mne.Evoked` objects.
+`~mne.Evoked` objects.
 
 .. contents:: Page contents
    :local:
@@ -38,7 +38,7 @@
     print(e.comment)
 
 ###############################################################################
-# To make our life easier, let's convert that list of :class:`~mne.Evoked`
+# To make our life easier, let's convert that list of `~mne.Evoked`
 # objects into a :class:`dictionary <dict>`. We'll use ``/``-separated
 # dictionary keys to encode the conditions (like is often done when epoching)
 # because some of the plotting methods can take advantage of that style of
@@ -60,8 +60,8 @@
 #    because the positive- and negative-going traces can resemble
 #    butterfly wings.
 #
-# The most basic plot of :class:`~mne.Evoked` objects is a butterfly plot of
-# each channel type, generated by the :meth:`evoked.plot() <mne.Evoked.plot>`
+# The most basic plot of `~mne.Evoked` objects is a butterfly plot of
+# each channel type, generated by the `evoked.plot() <mne.Evoked.plot>`
 # method. By default, channels marked as "bad" are suppressed, but you can
 # control this by passing an empty :class:`list` to the ``exclude`` parameter
 # (default is ``exclude='bads'``):
@@ -71,7 +71,7 @@
 ###############################################################################
 # Notice the completely flat EEG channel and the noisy gradiometer channel
 # plotted in red color. Like many MNE-Python plotting functions,
-# :meth:`evoked.plot() <mne.Evoked.plot>` has a ``picks`` parameter that can
+# `evoked.plot() <mne.Evoked.plot>` has a ``picks`` parameter that can
 # select channels to plot by name, index, or type. In the next plot we'll show
 # only magnetometer channels, and also color-code the channel traces by their
 # location by passing ``spatial_colors=True``. Finally, we'll superimpose a
@@ -87,7 +87,7 @@
 # click-dragged to select a time region, which will pop up a map of the average
 # field distribution over the scalp for the selected time span. You can also
 # generate scalp topographies at specific times or time spans using the
-# :meth:`~mne.Evoked.plot_topomap` method:
+# `~mne.Evoked.plot_topomap` method:
 
 times = np.linspace(0.05, 0.13, 5)
 evks['aud/left'].plot_topomap(ch_type='mag', times=times, colorbar=True)
@@ -107,7 +107,7 @@
 #
 # Scalp topographies at a given time point can be augmented with arrows to show
 # the estimated magnitude and direction of the magnetic field, using the
-# function :func:`mne.viz.plot_arrowmap`:
+# function `mne.viz.plot_arrowmap`:
 
 mags = evks['aud/left'].copy().pick_types(meg='mag')
 mne.viz.plot_arrowmap(mags.data[:, 175], mags.info, extrapolate='local')
@@ -126,17 +126,17 @@
 evks['vis/right'].plot_joint()
 
 ###############################################################################
-# Like :meth:`~mne.Evoked.plot_topomap` you can specify the ``times`` at which
+# Like `~mne.Evoked.plot_topomap` you can specify the ``times`` at which
 # you want the scalp topographies calculated, and you can customize the plot in
-# various other ways as well. See :meth:`mne.Evoked.plot_joint` for details.
+# various other ways as well. See `mne.Evoked.plot_joint` for details.
 #
 #
 # Comparing ``Evoked`` objects
 # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 #
-# To compare :class:`~mne.Evoked` objects from different experimental
-# conditions, the function :func:`mne.viz.plot_compare_evokeds` can take a
-# :class:`list` or :class:`dict` of :class:`~mne.Evoked` objects and plot them
+# To compare `~mne.Evoked` objects from different experimental
+# conditions, the function `mne.viz.plot_compare_evokeds` can take a
+# :class:`list` or :class:`dict` of `~mne.Evoked` objects and plot them
 # all on the same axes. Like most MNE-Python visualization functions, it has a
 # ``picks`` parameter for selecting channels, but by default will generate one
 # figure for each channel type, and combine information across channels of the
@@ -154,7 +154,7 @@ def custom_func(x):
     mne.viz.plot_compare_evokeds(evks, picks='eeg', combine=combine)
 
 ###############################################################################
-# One nice feature of :func:`~mne.viz.plot_compare_evokeds` is that when
+# One nice feature of `~mne.viz.plot_compare_evokeds` is that when
 # passing evokeds in a dictionary, it allows specifying plot styles based on
 # ``/``-separated substrings of the dictionary keys (similar to epoch
 # selection; see :ref:`tut-section-subselect-epochs`). Here, we specify colors
@@ -164,16 +164,35 @@ def custom_func(x):
 mne.viz.plot_compare_evokeds(evks, picks='MEG 1811', colors=dict(aud=0, vis=1),
                              linestyles=dict(left='solid', right='dashed'))
 
+###############################################################################
+# The legends generated by `~mne.viz.plot_compare_evokeds` above used the
+# dictionary keys provided by the ``evks`` variable. If instead you pass a
+# :class:`list` or :class:`tuple` of `~mne.Evoked` objects, the legend keys
+# will be generated automatically from the ``comment`` attribute of the
+# `~mne.Evoked` objects (or, as sequential integers if the comment attribute is
+# empty or ambiguous). To illustrate this, we'll make a list of 5 `~mne.Evoked`
+# objects: 2 with identical comments, 2 with empty comments (either an empty
+# string or ``None``), and 1 with a unique non-empty comment:
+
+temp_list = list()
+for idx, _comment in enumerate(('foo', 'foo', '', None, 'bar'), start=1):
+    _evk = evokeds_list[0].copy()
+    _evk.comment = _comment
+    _evk.data *= idx  # so we can tell the traces apart
+    temp_list.append(_evk)
+
+mne.viz.plot_compare_evokeds(temp_list, picks='mag')
+
 ###############################################################################
 # Image plots
 # ^^^^^^^^^^^
 #
-# Like :class:`~mne.Epochs`, :class:`~mne.Evoked` objects also have a
-# :meth:`~mne.Evoked.plot_image` method, but unlike :meth:`epochs.plot_image()
-# <mne.Epochs.plot_image>`, :meth:`evoked.plot_image() <mne.Evoked.plot_image>`
+# Like `~mne.Epochs`, `~mne.Evoked` objects also have a
+# `~mne.Evoked.plot_image` method, but unlike `epochs.plot_image()
+# <mne.Epochs.plot_image>`, `evoked.plot_image() <mne.Evoked.plot_image>`
 # shows one *channel* per row instead of one *epoch* per row. Again, a
 # ``picks`` parameter is available, as well as several other customization
-# options; see :meth:`~mne.Evoked.plot_image` for details.
+# options; see `~mne.Evoked.plot_image` for details.
 
 evks['vis/right'].plot_image(picks='meg')
 
@@ -182,7 +201,7 @@ def custom_func(x):
 # ^^^^^^^^^^^^^^^^^^^^^^
 #
 # For sensor-level analyses it can be useful to plot the response at each
-# sensor in a topographical layout. The :func:`~mne.viz.plot_compare_evokeds`
+# sensor in a topographical layout. The `~mne.viz.plot_compare_evokeds`
 # function can do this if you pass ``axes='topo'``, but it can be quite slow
 # if the number of sensors is too large, so here we'll plot only the EEG
 # channels:
@@ -193,20 +212,20 @@ def custom_func(x):
                                                       vis=dict(linewidth=1)))
 
 ###############################################################################
-# For larger numbers of sensors, the method :meth:`evoked.plot_topo()
-# <mne.Evoked.plot_topo>` and the function :func:`mne.viz.plot_evoked_topo`
-# can both be used. The :meth:`~mne.Evoked.plot_topo` method will plot only a
-# single condition, while the :func:`~mne.viz.plot_evoked_topo` function can
+# For larger numbers of sensors, the method `evoked.plot_topo()
+# <mne.Evoked.plot_topo>` and the function `mne.viz.plot_evoked_topo`
+# can both be used. The `~mne.Evoked.plot_topo` method will plot only a
+# single condition, while the `~mne.viz.plot_evoked_topo` function can
 # plot one or more conditions on the same axes, if passed a list of
-# :class:`~mne.Evoked` objects. The legend entries will be automatically drawn
-# from the :class:`~mne.Evoked` objects' ``comment`` attribute:
+# `~mne.Evoked` objects. The legend entries will be automatically drawn
+# from the `~mne.Evoked` objects' ``comment`` attribute:
 
 mne.viz.plot_evoked_topo(evokeds_list)
 
 ###############################################################################
-# By default, :func:`~mne.viz.plot_evoked_topo` will plot all MEG sensors (if
+# By default, `~mne.viz.plot_evoked_topo` will plot all MEG sensors (if
 # present), so to get EEG sensors you would need to modify the evoked objects
-# first (e.g., using :func:`mne.pick_types`).
+# first (e.g., using `mne.pick_types`).
 #
 # .. note::
 #
@@ -232,7 +251,7 @@ def custom_func(x):
 ###############################################################################
 # By default, MEG sensors will be used to estimate the field on the helmet
 # surface, while EEG sensors will be used to estimate the field on the scalp.
-# Once the maps are computed, you can plot them with :meth:`evoked.plot_field()
+# Once the maps are computed, you can plot them with `evoked.plot_field()
 # <mne.Evoked.plot_field>`:
 
 maps = mne.make_field_map(evks['aud/left'], trans=sample_data_trans_file,


Test patch: