Repository: mne-tools/mne-python

Issue:
Dropping epochs with Epochs.plot() not working properly
#### Describe the bug
During visual inspection of epoched EEG data I try to remove epochs with artifacts by clicking on them. After selecting all the unwanted epochs and closing the plot, I get a text like 

> Dropped X epochs: x_1, x_2, .... x_n

However, if I plot the same epochs object again, I find that the unwanted epochs are still there and, instead, other epochs adjacent to the unwanted one are missing. 

I also noticed that during the selection of unwanted epochs in the epochs.plot(), the selected epochs (marked in red) change to the epoch adjacent to the ones selected manually as I scroll over the data.


#### Steps to reproduce

```
import os
import mne

%matplotlib qt

sample_data_folder = mne.datasets.sample.data_path()
sample_data_raw_file = os.path.join(sample_data_folder, 'MEG', 'sample',
                                    'sample_audvis_raw.fif')
raw = mne.io.read_raw_fif(sample_data_raw_file, verbose=False).crop(tmax=60)

events = mne.find_events(raw, stim_channel='STI 014')

epochs = mne.Epochs(raw, events,event_id=3, tmin=-0.3, tmax=0.7)
del raw 

epochs.plot(n_epochs=5)

```

Mark some epochs as bad and scroll back and forward. See if the selected epochs are still the same (the epoch number does not change but the epoch is not the same anymore)

https://user-images.githubusercontent.com/45296438/121243779-7f27ac00-c874-11eb-843a-997acc4144d1.mp4

https://user-images.githubusercontent.com/45296438/121242587-1ee43a80-c873-11eb-9f97-81e007fca5d0.mp4


#### Expected results
*Drop the manually selected epochs and not others*


#### Actual results
*Epochs other than the selected ones are dropped*


#### Additional information*`mne.sys_info()`:

> Platform:      Windows-10-10.0.19041-SP0
Python:        3.9.2 | packaged by conda-forge | (default, Feb 21 2021, 04:59:43) [MSC v.1916 64 bit (AMD64)]
Executable:    C:\Users\mati_\Anaconda3\envs\mne\python.exe
CPU:           Intel64 Family 6 Model 78 Stepping 3, GenuineIntel: 4 cores
Memory:        7.9 GB

mne:           0.23.0
numpy:         1.20.1 {blas=mkl_rt, lapack=mkl_rt}
scipy:         1.6.2
matplotlib:    3.4.1 {backend=Qt5Agg}

sklearn:       0.24.1
numba:         0.53.1
nibabel:       3.2.1
nilearn:       0.7.1
dipy:          1.4.0
cupy:          Not found
pandas:        1.2.4
mayavi:        4.7.2
pyvista:       0.29.1 {pyvistaqt=0.3.0, OpenGL 4.5.0 - Build 23.20.16.4973 via Intel(R) HD Graphics 520}
vtk:           9.0.1
PyQt5:         5.12.3





Discussion:
Hello! 👋 Thanks for opening your first issue here! ❤️ We will try to get back to you soon. 🚴🏽‍♂️

I confirm there is a bug.  I haven't quite figured out the minimal sequence of interactions to reproduce it, but if I play around long enough I can get it to appear.  As far as I can tell so far, the correct epochs are getting dropped, and it's "just" a display problem.  Stay tuned as I continue debugging.
@drammock Thanks for your answer, I'll stay tuned. I wanted to add that I've had cases in which the selected epochs were not dropped. I actually realized something was wrong with my preprocessing (which included the visual inspection) when I started to see big outliers in posterior analysis such as time-frecuency representations of the data. I would come back to the preprocessing stage, find the epochs with the artifacts, drop them, save the epochs file, then do the TFRs to find that the outliers were still there.
@drammock I have noticed somethig on the first video that I uploaded. When I select the bad epoch, it is shown as epoch 149 and the following epoch is number 161 (originally my epochs where spaced by 3, so 152,155,..etc were missing). When I plot again, the bad epoch is still there with epoch number 161 and epoch 149 is gone. My drop_log showed a bunch of "user" dropped epochs in a row around epoch 161. It is possible that, by the time I selected the epoch, the visual shift had already occured so I was actually selecting the epochs wrong. By doing that a few times in a row, I might have deleted that bunch of epochs surrounding the bad epoch, and thats why the drop_log looked like that and all those epochs were missing in the plot. This is just a theory that I have now, I dont know if it is true but it might sustain the idea that it is "just" a display problem.

Resolving patch:
diff --git a/mne/viz/_figure.py b/mne/viz/_figure.py
--- a/mne/viz/_figure.py
+++ b/mne/viz/_figure.py
@@ -352,6 +352,8 @@ def __init__(self, inst, figsize, ica=None, xlabel='Time (s)', **kwargs):
         # additional params for epochs (won't affect raw / ICA)
         self.mne.epoch_traces = list()
         self.mne.bad_epochs = list()
+        self.mne.sampling_period = (np.diff(inst.times[:2])[0]
+                                    / inst.info['sfreq'])
         # annotations
         self.mne.annotations = list()
         self.mne.hscroll_annotations = list()
@@ -419,7 +421,7 @@ def __init__(self, inst, figsize, ica=None, xlabel='Time (s)', **kwargs):
             epoch_nums = self.mne.inst.selection
             for ix, _ in enumerate(epoch_nums):
                 start = self.mne.boundary_times[ix]
-                width = np.diff(self.mne.boundary_times[ix:ix + 2])[0]
+                width = np.diff(self.mne.boundary_times[:2])[0]
                 ax_hscroll.add_patch(
                     Rectangle((start, 0), width, 1, color='none',
                               zorder=self.mne.zorder['patch']))
@@ -1891,7 +1893,11 @@ def _load_data(self, start=None, stop=None):
         if 'raw' in (self.mne.instance_type, self.mne.ica_type):
             return self.mne.inst[:, start:stop]
         else:
-            ix = np.searchsorted(self.mne.boundary_times, self.mne.t_start)
+            # subtract one sample from tstart before searchsorted, to make sure
+            # we land on the left side of the boundary time (avoid precision
+            # errors)
+            ix = np.searchsorted(self.mne.boundary_times,
+                                 self.mne.t_start - self.mne.sampling_period)
             item = slice(ix, ix + self.mne.n_epochs)
             data = np.concatenate(self.mne.inst.get_data(item=item), axis=-1)
             times = np.arange(len(self.mne.inst) * len(self.mne.inst.times)
@@ -2039,8 +2045,8 @@ def _draw_traces(self):
             epoch_ix = np.searchsorted(self.mne.boundary_times, time_range)
             epoch_ix = np.arange(epoch_ix[0], epoch_ix[1])
             epoch_nums = self.mne.inst.selection[epoch_ix[0]:epoch_ix[-1] + 1]
-            visible_bad_epochs = epoch_nums[
-                np.in1d(epoch_nums, self.mne.bad_epochs).nonzero()]
+            visible_bad_epoch_ix, = np.in1d(
+                epoch_nums, self.mne.bad_epochs).nonzero()
             while len(self.mne.epoch_traces):
                 self.mne.epoch_traces.pop(-1).remove()
             # handle custom epoch colors (for autoreject integration)
@@ -2056,8 +2062,7 @@ def _draw_traces(self):
                     custom_colors[:, ii] = to_rgba_array([this_colors[_ch]
                                                           for _ch in picks])
             # override custom color on bad epochs
-            for _bad in visible_bad_epochs:
-                _ix = epoch_nums.tolist().index(_bad)
+            for _ix in visible_bad_epoch_ix:
                 _cols = np.array([self.mne.epoch_color_bad,
                                   self.mne.ch_color_bad])[bad_bool.astype(int)]
                 custom_colors[:, _ix] = to_rgba_array(_cols)


Test patch: