Repository: mne-tools/mne-python

Issue:
plotting overlapping epochs: Events not properly aligned in plot?
I have to create overlapping epochs from scratch using [EpochsArray](https://mne.tools/dev/generated/mne.EpochsArray.html#mne.EpochsArray). I designed my events, tmin, and other parameters so that the main event for each epoch (the t0 event) is exactly in the middle of a 2 second timespan (-1s to 1s). However when plotting via `epochs.plot`, the events are **not** in the middle.

This plotting appearance is fixed when I subset my epochs to only contain those epochs that are not overlapping.

The following MWE is modified from [the docs](https://mne.tools/dev/auto_tutorials/simulation/10_array_objs.html#creating-mne-python-data-structures-from-scratch).

The only change from the docs example is to go from non-overlapping epochs: `np.arange(0, 1000, sampling_freq)` 
to overlapping epochs: `np.linspace(0, 200, 5, dtype=int)`.

```python
import mne
import numpy as np

sampling_freq = 200  # in Hertz

info = mne.create_info(ch_names=['10 Hz sine', '5 Hz cosine'],
                       ch_types=['misc'] * 2,
                       sfreq=sampling_freq)

times = np.linspace(0, 1, sampling_freq, endpoint=False)
sine = np.sin(20 * np.pi * times)
cosine = np.cos(10 * np.pi * times)

data = np.array([[0.2 * sine, 1.0 * cosine],
                 [0.4 * sine, 0.8 * cosine],
                 [0.6 * sine, 0.6 * cosine],
                 [0.8 * sine, 0.4 * cosine],
                 [1.0 * sine, 0.2 * cosine]])


events = np.column_stack((np.linspace(0, 200, 5, dtype=int),  # NOTE: used to be np.arange(0, 1000, sampling_freq),
                          np.zeros(5, dtype=int),
                          np.array([1, 2, 1, 2, 1])))
event_dict = dict(condition_A=1, condition_B=2)
simulated_epochs = mne.EpochsArray(data, info, tmin=-0.5, events=events,
                                   event_id=event_dict)

simulated_epochs.plot(picks='misc', events=events, event_id=event_dict)

```

Take a look at the resulting plot. It's hard to see in the following screenshot, but the events are NOT centered in the middle of the epoch -> rather, they are clustered at the end of the plot:

![image](https://user-images.githubusercontent.com/9084751/123261599-3d933400-d4f7-11eb-9af2-add4db20653c.png)


When subsetting these epochs to only contain non-overlapping epochs, the issue is solved and the plot shows the expected alignment of events in the middle of epochs.

```python
simulated_epochs[np.array((0, 4))].plot(picks='misc', events=events[np.array((0, 4))], event_id=event_dict)

```

![image](https://user-images.githubusercontent.com/9084751/123261694-5b609900-d4f7-11eb-8e8c-1940da221c22.png)


**Is this somehow expected? Or is it a bug?**


Discussion:
there is definitely a bug, but also a difficult question: if a single event would be visible in multiple epochs, should it be shown in all of them?  Options:

1. yes, show an event in multiple epochs if appropriate
2. no, only show it in the sequentially first epoch where it appears
3. no, only show it in the epoch that it "belongs" to
 
I think number 3 is impossible because `events` are passed in separately so we would have to guess/infer which event "belongs" to which epoch (not so easy for things like errant button presses, or in cases like this where epochs overlap).  I will try to solve this as if number 1 is the correct approach, but please weigh in if you think number 2 is actually better.  Another question is whether we should log an info or warning message when events are duplicated in the figure?
Thanks for picking this up! I think your option 1 is most appropriate, and I agree that a log info is probably a good idea as well.

Resolving patch:
diff --git a/mne/viz/epochs.py b/mne/viz/epochs.py
--- a/mne/viz/epochs.py
+++ b/mne/viz/epochs.py
@@ -795,16 +795,29 @@ def plot_epochs(epochs, picks=None, scalings=None, n_epochs=20, n_channels=20,
     # events
     if events is not None:
         event_nums = events[:, 2]
-        boundary_samps = epochs.events[1:, 0] - epochs.time_as_index(0)
-        event_ixs = np.searchsorted(boundary_samps, events[:, 0], side='right')
-        event_samp_offsets = (events[:, 0] - epochs.events[:, 0][event_ixs]
-                              + epochs.time_as_index(0))
-        event_times = ((np.arange(len(epochs)) * len(epochs.times))[event_ixs]
-                       + event_samp_offsets) / sfreq
-        # don't show events that are beyond the range of their epoch
-        mask = event_samp_offsets <= len(epochs.times)
-        event_times = event_times[mask]
-        event_nums = event_nums[mask]
+        event_samps = events[:, 0]
+        epoch_n_samps = len(epochs.times)
+        # handle overlapping epochs (each event may show up in multiple places)
+        boundaries = (epochs.events[:, [0]] + np.array([-1, 1])
+                      * epochs.time_as_index(0))
+        in_bounds = np.logical_and(boundaries[:, [0]] <= event_samps,
+                                   event_samps < boundaries[:, [1]])
+        event_ixs = [np.nonzero(a)[0] for a in in_bounds.T]
+        warned = False
+        event_times = list()
+        event_numbers = list()
+        for samp, num, _ixs in zip(event_samps, event_nums, event_ixs):
+            relevant_epoch_events = epochs.events[:, 0][_ixs]
+            if len(relevant_epoch_events) > 1 and not warned:
+                logger.info('You seem to have overlapping epochs. Some event '
+                            'lines may be duplicated in the plot.')
+                warned = True
+            offsets = samp - relevant_epoch_events + epochs.time_as_index(0)
+            this_event_times = (_ixs * epoch_n_samps + offsets) / sfreq
+            event_times.extend(this_event_times)
+            event_numbers.extend([num] * len(_ixs))
+        event_nums = np.array(event_numbers)
+        event_times = np.array(event_times)
     else:
         event_nums = None
         event_times = None


Test patch:
diff --git a/mne/viz/tests/test_epochs.py b/mne/viz/tests/test_epochs.py
--- a/mne/viz/tests/test_epochs.py
+++ b/mne/viz/tests/test_epochs.py
@@ -219,6 +219,19 @@ def test_plot_epochs_keypresses():
     _fake_click(fig, data_ax, [x, y], xform='data', button=3)  # remove vlines
 
 
+def test_plot_overlapping_epochs_with_events():
+    """Test drawing of event lines in overlapping epochs."""
+    data = np.zeros(shape=(3, 2, 100))  # 3 epochs, 2 channels, 100 samples
+    sfreq = 100
+    info = create_info(
+        ch_names=('a', 'b'), ch_types=('misc', 'misc'), sfreq=sfreq)
+    # 90% overlap, so all 3 events should appear in all 3 epochs when plotted:
+    events = np.column_stack(([50, 60, 70], [0, 0, 0], [1, 2, 3]))
+    epochs = EpochsArray(data, info, tmin=-0.5, events=events)
+    fig = epochs.plot(events=events, picks='misc')
+    assert len(fig.mne.event_lines.get_segments()) == 9
+
+
 def test_epochs_plot_sensors(epochs):
     """Test sensor plotting."""
     epochs.plot_sensors()