Repository: mne-tools/mne-python

Issue:
epochs.plot(..., event_colors) not checking epochs.event_id
#### Describe the bug
`epochs.plot(..., event_colors)` raises value error when `event_colors` uses keys that are in `event_id` dict

#### Steps to reproduce
```python console
In [1]: import os                                                                                                           
In [2]: import mne                                                                                                          
In [3]: sample_data_folder = mne.datasets.sample.data_path()                                                                
In [4]: sample_data_raw_file = os.path.join(sample_data_folder, 'MEG', 'sample', 
   ...:                                     'sample_audvis_raw.fif')                                                        
In [5]: raw = mne.io.read_raw_fif(sample_data_raw_file, verbose=False).crop(tmax=120)                                       
In [6]: events = mne.find_events(raw, stim_channel='STI 014')                                                               
# 176 events found
# Event IDs: [ 1  2  3  4  5 32]
In [7]: event_dict = {'auditory/left': 1, 'auditory/right': 2, 'visual/left': 3, 
   ...:               'visual/right': 4, 'face': 5, 'buttonpress': 32} 
In [8]: epochs = mne.Epochs(raw, events, tmin=-0.2, tmax=0.5, event_id=event_dict, 
   ...:                     preload=True)                                                                                   
# 176 matching events found
# Applying baseline correction (mode: mean)
# Not setting metadata
# Created an SSP operator (subspace dimension = 3)
# 3 projection items activated
# Loading data for 176 events and 421 original time points ...
# 1 bad epochs dropped
In [9]: epochs['face'].plot(events=events, event_colors={'buttonpress': 'red'})                                             
# 〈TRACEBACK OMITTED FOR BREVITY〉
# ValueError: Event ID buttonpress is in the color dict but is not present in events or event_id.
In [10]: epochs.event_id                                                                                                    
Out[10]: 
# {'auditory/left': 1,
#  'auditory/right': 2,
#  'visual/left': 3,
#  'visual/right': 4,
#  'face': 5,
#  'buttonpress': 32}
```

#### Expected results
`epochs.plot(..., event_colors)` should check its keys against keys in `epochs.event_id` and plot the event lines (in this case event 32) in the requested color.


#### Actual results
Here's the full traceback

<details>

```
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-9-92c02e7b010c> in <module>
----> 1 epochs['face'].plot(events=events, event_colors={'buttonpress': 'red'})

/opt/mne/python/mne/epochs.py in plot(self, picks, scalings, n_epochs, n_channels, title, events, event_colors, order, show, block, decim, noise_cov, butterfly, show_scrollbars, epoch_colors)
   1051                            noise_cov=noise_cov, butterfly=butterfly,
   1052                            show_scrollbars=show_scrollbars,
-> 1053                            epoch_colors=epoch_colors)
   1054 
   1055     @copy_function_doc_to_method_doc(plot_epochs_psd)

/opt/mne/python/mne/viz/epochs.py in plot_epochs(epochs, picks, scalings, n_epochs, n_channels, title, events, event_colors, order, show, block, decim, noise_cov, butterfly, show_scrollbars, epoch_colors)
    850     _prepare_mne_browse_epochs(params, projs, n_channels, n_epochs, scalings,
    851                                title, picks, events=events, order=order,
--> 852                                event_colors=event_colors, butterfly=butterfly)
    853     _prepare_projectors(params)
    854 

/opt/mne/python/mne/viz/epochs.py in _prepare_mne_browse_epochs(params, projs, n_channels, n_epochs, scalings, title, picks, events, event_colors, order, butterfly, info)
   1110     if events is not None:
   1111         event_set = set(events[:, 2])
-> 1112         event_colors = _handle_event_colors(event_set, event_colors, event_set)
   1113         epoch_nr = False  # epoch number off by default to avoid overlap
   1114         for label in ax.xaxis.get_ticklabels():

/opt/mne/python/mne/viz/misc.py in _handle_event_colors(unique_events, color, unique_events_id)
   1035             if this_event not in unique_events_id:
   1036                 raise ValueError('Event ID %s is in the color dict but is not '
-> 1037                                  'present in events or event_id.' % this_event)
   1038 
   1039         for this_event in unique_events_id:

ValueError: Event ID buttonpress is in the color dict but is not present in events or event_id.
```

</details>


#### Additional information

`mne.sys_info()`

<details>
```
Platform:      Linux-4.15.0-70-generic-x86_64-with-debian-buster-sid
Python:        3.7.3 | packaged by conda-forge | (default, Mar 27 2019, 23:01:00)  [GCC 7.3.0]
Executable:    /opt/miniconda3/envs/mnedev/bin/python
CPU:           x86_64: 8 cores
Memory:        7.6 GB

mne:           0.20.dev0
numpy:         1.16.4 {blas=blas, lapack=blas}
scipy:         1.3.0
matplotlib:    3.1.0 {backend=Qt5Agg}

sklearn:       0.21.2
numba:         Not found
nibabel:       2.4.1
cupy:          Not found
pandas:        0.24.2
dipy:          0.16.0
mayavi:        4.7.1.dev0 {qt_api=pyqt5, PyQt5=5.9.2}
pyvista:       0.20.4
vtk:           8.1.2
```
</details>




Discussion:
Yes this looks like a bug, feel free to make a PR to fix if you have time otherwise we can squash it later

Resolving patch:
diff --git a/mne/viz/epochs.py b/mne/viz/epochs.py
--- a/mne/viz/epochs.py
+++ b/mne/viz/epochs.py
@@ -1109,7 +1109,8 @@ def _prepare_mne_browse_epochs(params, projs, n_channels, n_epochs, scalings,
     epoch_nr = True
     if events is not None:
         event_set = set(events[:, 2])
-        event_colors = _handle_event_colors(event_set, event_colors, event_set)
+        event_colors = _handle_event_colors(event_colors, event_set,
+                                            params['epochs'].event_id)
         epoch_nr = False  # epoch number off by default to avoid overlap
         for label in ax.xaxis.get_ticklabels():
             label.set_visible(False)
diff --git a/mne/viz/misc.py b/mne/viz/misc.py
--- a/mne/viz/misc.py
+++ b/mne/viz/misc.py
@@ -31,7 +31,7 @@
 from ..source_space import read_source_spaces, SourceSpaces, _read_mri_info
 from ..transforms import invert_transform, apply_trans
 from ..utils import (logger, verbose, get_subjects_dir, warn, _check_option,
-                     _mask_to_onsets_offsets)
+                     _mask_to_onsets_offsets, _pl)
 from ..io.pick import _picks_by_type
 from ..filter import estimate_ringing_samples
 from .utils import tight_layout, _get_color_list, _prepare_trellis, plt_show
@@ -585,7 +585,7 @@ def plot_events(events, sfreq=None, first_samp=0, color=None, event_id=None,
     else:
         unique_events_id = unique_events
 
-    color = _handle_event_colors(unique_events, color, unique_events_id)
+    color = _handle_event_colors(color, unique_events, event_id)
     import matplotlib.pyplot as plt
 
     fig = None
@@ -1020,27 +1020,32 @@ def plot_ideal_filter(freq, gain, axes=None, title='', flim=None, fscale='log',
     return axes.figure
 
 
-def _handle_event_colors(unique_events, color, unique_events_id):
-    """Handle event colors."""
-    if color is None:
+def _handle_event_colors(color_dict, unique_events, event_id):
+    """Create event-integer-to-color mapping, assigning defaults as needed."""
+    default_colors = dict(zip(sorted(unique_events), cycle(_get_color_list())))
+    # warn if not enough colors
+    if color_dict is None:
         if len(unique_events) > len(_get_color_list()):
-            warn('More events than colors available. You should pass a list '
-                 'of unique colors.')
-        colors = cycle(_get_color_list())
-        color = dict()
-        for this_event, this_color in zip(sorted(unique_events_id), colors):
-            color[this_event] = this_color
+            warn('More events than default colors available. You should pass '
+                 'a list of unique colors.')
     else:
-        for this_event in color:
-            if this_event not in unique_events_id:
-                raise ValueError('Event ID %s is in the color dict but is not '
-                                 'present in events or event_id.' % this_event)
-
-        for this_event in unique_events_id:
-            if this_event not in color:
-                warn('Color is not available for event %d. Default colors '
-                     'will be used.' % this_event)
-    return color
+        custom_colors = dict()
+        for key, color in color_dict.items():
+            if key in unique_events:  # key was a valid event integer
+                custom_colors[key] = color
+            elif key in event_id:     # key was an event label
+                custom_colors[event_id[key]] = color
+            else:                     # key not a valid event, warn and ignore
+                warn('Event ID %s is in the color dict but is not '
+                     'present in events or event_id.' % str(key))
+        # warn if color_dict is missing any entries
+        unassigned = sorted(set(unique_events) - set(custom_colors))
+        if len(unassigned):
+            unassigned_str = ', '.join(str(e) for e in unassigned)
+            warn('Color was not assigned for event%s %s. Default colors will '
+                 'be used.' % (_pl(unassigned), unassigned_str))
+        default_colors.update(custom_colors)
+    return default_colors
 
 
 def plot_csd(csd, info=None, mode='csd', colorbar=True, cmap=None,
diff --git a/tutorials/epochs/plot_20_visualize_epochs.py b/tutorials/epochs/plot_20_visualize_epochs.py
--- a/tutorials/epochs/plot_20_visualize_epochs.py
+++ b/tutorials/epochs/plot_20_visualize_epochs.py
@@ -68,7 +68,7 @@
 
 catch_trials_and_buttonpresses = mne.pick_events(events, include=[5, 32])
 epochs['face'].plot(events=catch_trials_and_buttonpresses,
-                    event_colors={32: 'red', 5: 'yellow'})
+                    event_colors={32: 'red', 5: 'green'})
 
 ###############################################################################
 # Plotting projectors from an ``Epochs`` object


Test patch:
diff --git a/mne/viz/tests/test_misc.py b/mne/viz/tests/test_misc.py
--- a/mne/viz/tests/test_misc.py
+++ b/mne/viz/tests/test_misc.py
@@ -14,13 +14,15 @@
 import matplotlib.pyplot as plt
 
 from mne import (read_events, read_cov, read_source_spaces, read_evokeds,
-                 read_dipole, SourceEstimate)
+                 read_dipole, SourceEstimate, pick_events)
 from mne.datasets import testing
 from mne.filter import create_filter
 from mne.io import read_raw_fif
 from mne.minimum_norm import read_inverse_operator
 from mne.viz import (plot_bem, plot_events, plot_source_spectrogram,
                      plot_snr_estimate, plot_filter, plot_csd)
+from mne.viz.misc import _handle_event_colors
+from mne.viz.utils import _get_color_list
 from mne.utils import requires_nibabel, run_tests_if_main
 from mne.time_frequency import CrossSpectralDensity
 
@@ -100,6 +102,22 @@ def test_plot_bem():
              orientation='coronal', slices=[25, 50], src=src_fname)
 
 
+def test_event_colors():
+    """Test color assignment."""
+    events = pick_events(_get_events(), include=[1, 2])
+    unique_events = set(events[:, 2])
+    # make sure defaults work
+    colors = _handle_event_colors(None, unique_events, dict())
+    default_colors = _get_color_list()
+    assert colors[1] == default_colors[0]
+    # make sure custom color overrides default
+    colors = _handle_event_colors(color_dict=dict(foo='k', bar='#facade'),
+                                  unique_events=unique_events,
+                                  event_id=dict(foo=1, bar=2))
+    assert colors[1] == 'k'
+    assert colors[2] == '#facade'
+
+
 def test_plot_events():
     """Test plotting events."""
     event_labels = {'aud_l': 1, 'aud_r': 2, 'vis_l': 3, 'vis_r': 4}
@@ -113,17 +131,19 @@ def test_plot_events():
     with pytest.warns(RuntimeWarning, match='will be ignored'):
         plot_events(events, raw.info['sfreq'], raw.first_samp,
                     event_id=event_labels)
-    with pytest.warns(RuntimeWarning, match='Color is not available'):
+    with pytest.warns(RuntimeWarning, match='Color was not assigned'):
         plot_events(events, raw.info['sfreq'], raw.first_samp,
                     color=color)
-    with pytest.warns(RuntimeWarning, match='event .* missing'):
+    with pytest.warns(RuntimeWarning, match=r'vent \d+ missing from event_id'):
         plot_events(events, raw.info['sfreq'], raw.first_samp,
                     event_id=event_labels, color=color)
-    with pytest.warns(RuntimeWarning, match='event .* missing'):
-        pytest.raises(ValueError, plot_events, events, raw.info['sfreq'],
-                      raw.first_samp, event_id={'aud_l': 1}, color=color)
-    pytest.raises(ValueError, plot_events, events, raw.info['sfreq'],
-                  raw.first_samp, event_id={'aud_l': 111}, color=color)
+    multimatch = r'event \d+ missing from event_id|in the color dict but is'
+    with pytest.warns(RuntimeWarning, match=multimatch):
+        plot_events(events, raw.info['sfreq'], raw.first_samp,
+                    event_id={'aud_l': 1}, color=color)
+    with pytest.raises(ValueError, match='from event_id is not present in'):
+        plot_events(events, raw.info['sfreq'], raw.first_samp,
+                    event_id={'aud_l': 111}, color=color)
     plt.close('all')