|
| 1 | +""" |
| 2 | +============================================================== |
| 3 | +Advanced plotting customization by subclassing MNEBrowseFigure |
| 4 | +============================================================== |
| 5 | +
|
| 6 | +This example shows how plot_epochs(...) and plot_raw(...) can be customized by |
| 7 | +subclassing MNEBrowseFigure and using the ``figure_class`` argument. |
| 8 | +It plots one EEG trace overlaid ("onion-skinned") on top of another. |
| 9 | +
|
| 10 | +This example is "bad code" in a few ways: |
| 11 | +
|
| 12 | +* Since the interface for MNEBrowseFigure is not public, it is liable to |
| 13 | + break between minor and even patch versions of MNE without warning |
| 14 | +* Some functionality is reimplemented from MNEBrowseFigure in a more or |
| 15 | + less copy-paste style |
| 16 | +* The code is backend-specific, in particular it is limited to the |
| 17 | + matplotlib backend, and will not work with the qt browser |
| 18 | +* Since there is no way to pass another EEG directly to the MNEBrowseFigure, |
| 19 | + it is passed through a global variable |
| 20 | +
|
| 21 | +Nevertheless, the example shows that the "escape hatch" of using a subclass is |
| 22 | +available when other customization possibilities offered by MNE are not |
| 23 | +sufficient. |
| 24 | +""" |
| 25 | + |
| 26 | +# Authors: The MNE-Python contributors. |
| 27 | +# License: BSD-3-Clause |
| 28 | +# Copyright the MNE-Python contributors. |
| 29 | + |
| 30 | +from mne.datasets import eegbci |
| 31 | +from mne.io import read_raw_edf |
| 32 | +from mne.viz import set_browser_backend |
| 33 | +from mne.viz._mpl_figure import MNEBrowseFigure as MNEBrowseFigureOrig |
| 34 | + |
| 35 | +set_browser_backend("matplotlib") |
| 36 | + |
| 37 | + |
| 38 | +onionskin_eeg = None |
| 39 | + |
| 40 | + |
| 41 | +def _set_onionskin_eeg(eeg): |
| 42 | + global onionskin_eeg |
| 43 | + onionskin_eeg = eeg |
| 44 | + |
| 45 | + |
| 46 | +class OnionskinMNEBrowseFigure(MNEBrowseFigureOrig): |
| 47 | + """ |
| 48 | + Subclass of MNEBrowseFigure adding in onion-skin functionality, |
| 49 | + i.e. plotting one EEG trace overlaid on top of another. |
| 50 | + """ |
| 51 | + |
| 52 | + def __init__(self, *args, **kwargs): |
| 53 | + import numpy as np |
| 54 | + |
| 55 | + super().__init__(*args, **kwargs) |
| 56 | + onionskin_kwargs = { |
| 57 | + **self.mne.trace_kwargs, |
| 58 | + } |
| 59 | + self.mne.onionskins = self.mne.ax_main.plot( |
| 60 | + np.full((1, self.mne.n_channels), np.nan), **onionskin_kwargs |
| 61 | + ) |
| 62 | + |
| 63 | + def _update_data(self): |
| 64 | + import numpy as np |
| 65 | + |
| 66 | + from mne.io.base import BaseRaw |
| 67 | + |
| 68 | + super()._update_data() |
| 69 | + if not onionskin_eeg: |
| 70 | + self.mne.onionskin_data = None |
| 71 | + return |
| 72 | + start, stop = self._get_start_stop() |
| 73 | + if isinstance(onionskin_eeg, BaseRaw): |
| 74 | + if stop is None: |
| 75 | + data = onionskin_eeg[:, start:] |
| 76 | + else: |
| 77 | + data = onionskin_eeg[:, start:stop] |
| 78 | + data = data[0] |
| 79 | + else: |
| 80 | + ix_start = np.searchsorted( |
| 81 | + self.mne.boundary_times, self.mne.t_start - self.mne.sampling_period |
| 82 | + ) |
| 83 | + ix_stop = ix_start + self.mne.n_epochs |
| 84 | + item = slice(ix_start, ix_stop) |
| 85 | + print(type(onionskin_eeg)) |
| 86 | + data = np.concatenate( |
| 87 | + onionskin_eeg.get_data(item=item, copy=False), axis=-1 |
| 88 | + ) |
| 89 | + data = self._process_data(data, start, stop, picks=self.mne.picks) |
| 90 | + self.mne.onionskin_data = data |
| 91 | + |
| 92 | + def _draw_traces(self): |
| 93 | + import numpy as np |
| 94 | + from matplotlib.colors import to_rgba_array |
| 95 | + from matplotlib.patches import Rectangle |
| 96 | + |
| 97 | + super()._draw_traces() |
| 98 | + if self.mne.onionskin_data is None: |
| 99 | + return |
| 100 | + picks = self.mne.picks |
| 101 | + offset_ixs = ( |
| 102 | + picks |
| 103 | + if self.mne.butterfly and self.mne.ch_selections is None |
| 104 | + else slice(None) |
| 105 | + ) |
| 106 | + offsets = self.mne.trace_offsets[offset_ixs] |
| 107 | + |
| 108 | + ch_colors = to_rgba_array(self.mne.ch_colors) |
| 109 | + ch_colors[:, 3] *= 0.5 |
| 110 | + |
| 111 | + decim = np.ones_like(picks) |
| 112 | + data_picks_mask = np.isin(picks, self.mne.picks_data) |
| 113 | + decim[data_picks_mask] = self.mne.decim |
| 114 | + # decim can vary by channel type, so compute different `times` vectors |
| 115 | + decim_times = { |
| 116 | + decim_value: self.mne.times[::decim_value] + self.mne.first_time |
| 117 | + for decim_value in set(decim) |
| 118 | + } |
| 119 | + |
| 120 | + time_range = (self.mne.times + self.mne.first_time)[[0, -1]] |
| 121 | + ylim = self.mne.ax_main.get_ylim() |
| 122 | + for ii, line in enumerate(self.mne.onionskins): |
| 123 | + this_offset = offsets[ii] |
| 124 | + this_times = decim_times[decim[ii]] |
| 125 | + this_data = ( |
| 126 | + this_offset - self.mne.onionskin_data[ii] * self.mne.scale_factor |
| 127 | + ) |
| 128 | + this_data = this_data[..., :: decim[ii]] |
| 129 | + clip = 0.2 if self.mne.butterfly else 0.5 |
| 130 | + bottom = max(this_offset - clip, ylim[1]) |
| 131 | + height = min(2 * clip, ylim[0] - bottom) |
| 132 | + rect = Rectangle( |
| 133 | + xy=np.array([time_range[0], bottom]), |
| 134 | + width=time_range[1] - time_range[0], |
| 135 | + height=height, |
| 136 | + transform=self.mne.ax_main.transData, |
| 137 | + ) |
| 138 | + line.set_clip_path(rect) |
| 139 | + line.set_xdata(this_times) |
| 140 | + line.set_ydata(this_data) |
| 141 | + color = ch_colors[ii] |
| 142 | + line.set_color(color) |
| 143 | + line.set_zorder(self.mne.zorder["data"] - 1) |
| 144 | + |
| 145 | + |
| 146 | +subjects = [1] |
| 147 | +runs = [1, 2] |
| 148 | +raw_fnames = eegbci.load_data(subjects, runs) |
| 149 | +first_data = read_raw_edf(raw_fnames[0], preload=True) |
| 150 | +second_data = read_raw_edf(raw_fnames[1], preload=True) |
| 151 | + |
| 152 | +first_data.plot(title="First plot", n_channels=3) |
| 153 | +second_data.plot(title="Second plot", n_channels=3) |
| 154 | + |
| 155 | +_set_onionskin_eeg(first_data) |
| 156 | +second_data.plot( |
| 157 | + title="Onionskinned plot", |
| 158 | + n_channels=3, |
| 159 | + block=True, |
| 160 | + figure_class=OnionskinMNEBrowseFigure, |
| 161 | +) |
0 commit comments