-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Allow subclasses of FigureClass to be passed to plot_raw/plot_epochs #13979
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| Allow subclasses of `MNEBrowseFigure` to be passed to plot_raw/plot_epochs, as well as the corresponding `plot(...)` methods of the raw and epochs classes, by :newcontrib:`Frankie Robertson` | ||
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,149 @@ | ||||||||||||
| """ | ||||||||||||
| .. _ex-arrowmap: | ||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wrong label, you can just remove it unless you specifically want to link to it in the docs |
||||||||||||
|
|
||||||||||||
| ============================================================== | ||||||||||||
| Advanced plotting customization by subclassing MNEBrowseFigure | ||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||||||||
| ============================================================== | ||||||||||||
|
|
||||||||||||
| This example shows how plot_epochs(...) and plot_raw(...) can be customized by | ||||||||||||
| subclassing MNEBrowseFigure and using the `figure_class` argument. | ||||||||||||
| It plots one EEG trace overlaid ("onion-skinned") on top of another. | ||||||||||||
|
|
||||||||||||
| This example is "bad code" in a few ways: | ||||||||||||
| * Since the interface for MNEBrowseFigure is not public, it is liable to | ||||||||||||
|
Comment on lines
+12
to
+13
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This currently renders as a definition list, which I don't think is what you meant? If supposed to just be bullets then
Suggested change
and fix other |
||||||||||||
| break between minor and even patch versions of MNE without warning | ||||||||||||
| * Some functionality is reimplemented from MNEBrowseFigure in a more or | ||||||||||||
| less copy-paste style | ||||||||||||
| * The code is backend-specific, in particular it is limited to the | ||||||||||||
| matplotlib backend, and will not work with the qt browser | ||||||||||||
| * Since there is no way to pass another EEG directly to the MNEBrowseFigure, | ||||||||||||
| it is passed through a global variable | ||||||||||||
|
|
||||||||||||
| Nevertheless, the example shows that the "escape hatch" of using a subclass is | ||||||||||||
| available when other customization possibilities offered by MNE are not | ||||||||||||
| sufficient. | ||||||||||||
| """ | ||||||||||||
|
|
||||||||||||
| from mne.datasets import eegbci | ||||||||||||
| from mne.io import read_raw_edf | ||||||||||||
| from mne.viz import set_browser_backend | ||||||||||||
| from mne.viz._mpl_figure import MNEBrowseFigure as MNEBrowseFigureOrig | ||||||||||||
|
|
||||||||||||
| set_browser_backend("matplotlib") | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| onionskin_eeg = None | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| def _set_onionskin_eeg(eeg): | ||||||||||||
| global onionskin_eeg | ||||||||||||
| onionskin_eeg = eeg | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| class OnionskinMNEBrowseFigure(MNEBrowseFigureOrig): | ||||||||||||
| """ | ||||||||||||
| Subclass of MNEBrowseFigure adding in onion-skin functionality, | ||||||||||||
| i.e. plotting one EEG trace overlaid on top of another. | ||||||||||||
| """ | ||||||||||||
|
|
||||||||||||
| def __init__(self, *args, **kwargs): | ||||||||||||
| import numpy as np | ||||||||||||
|
|
||||||||||||
| super().__init__(*args, **kwargs) | ||||||||||||
| onionskin_kwargs = { | ||||||||||||
| **self.mne.trace_kwargs, | ||||||||||||
| } | ||||||||||||
| self.mne.onionskins = self.mne.ax_main.plot( | ||||||||||||
| np.full((1, self.mne.n_channels), np.nan), **onionskin_kwargs | ||||||||||||
| ) | ||||||||||||
|
|
||||||||||||
| def _update_data(self): | ||||||||||||
| import numpy as np | ||||||||||||
|
|
||||||||||||
| from mne.io.base import BaseRaw | ||||||||||||
|
|
||||||||||||
| super()._update_data() | ||||||||||||
| if not onionskin_eeg: | ||||||||||||
| self.mne.onionskin_data = None | ||||||||||||
| return | ||||||||||||
| start, stop = self._get_start_stop() | ||||||||||||
| if isinstance(onionskin_eeg, BaseRaw): | ||||||||||||
| if stop is None: | ||||||||||||
| data = onionskin_eeg[:, start:] | ||||||||||||
| else: | ||||||||||||
| data = onionskin_eeg[:, start:stop] | ||||||||||||
| data = data[0] | ||||||||||||
| else: | ||||||||||||
| ix_start = np.searchsorted( | ||||||||||||
| self.mne.boundary_times, self.mne.t_start - self.mne.sampling_period | ||||||||||||
| ) | ||||||||||||
| ix_stop = ix_start + self.mne.n_epochs | ||||||||||||
| item = slice(ix_start, ix_stop) | ||||||||||||
| print(type(onionskin_eeg)) | ||||||||||||
| data = np.concatenate( | ||||||||||||
| onionskin_eeg.get_data(item=item, copy=False), axis=-1 | ||||||||||||
| ) | ||||||||||||
| data = self._process_data(data, start, stop, picks=self.mne.picks) | ||||||||||||
| self.mne.onionskin_data = data | ||||||||||||
|
|
||||||||||||
| def _draw_traces(self): | ||||||||||||
| import numpy as np | ||||||||||||
| from matplotlib.colors import to_rgba_array | ||||||||||||
| from matplotlib.patches import Rectangle | ||||||||||||
|
|
||||||||||||
| super()._draw_traces() | ||||||||||||
| if self.mne.onionskin_data is None: | ||||||||||||
| return | ||||||||||||
| picks = self.mne.picks | ||||||||||||
| offset_ixs = ( | ||||||||||||
| picks | ||||||||||||
| if self.mne.butterfly and self.mne.ch_selections is None | ||||||||||||
| else slice(None) | ||||||||||||
| ) | ||||||||||||
| offsets = self.mne.trace_offsets[offset_ixs] | ||||||||||||
|
|
||||||||||||
| ch_colors = to_rgba_array(self.mne.ch_colors) | ||||||||||||
| ch_colors[:, 3] *= 0.5 | ||||||||||||
|
|
||||||||||||
| decim = np.ones_like(picks) | ||||||||||||
| data_picks_mask = np.isin(picks, self.mne.picks_data) | ||||||||||||
| decim[data_picks_mask] = self.mne.decim | ||||||||||||
| # decim can vary by channel type, so compute different `times` vectors | ||||||||||||
| decim_times = { | ||||||||||||
| decim_value: self.mne.times[::decim_value] + self.mne.first_time | ||||||||||||
| for decim_value in set(decim) | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| time_range = (self.mne.times + self.mne.first_time)[[0, -1]] | ||||||||||||
| ylim = self.mne.ax_main.get_ylim() | ||||||||||||
| for ii, line in enumerate(self.mne.onionskins): | ||||||||||||
| this_offset = offsets[ii] | ||||||||||||
| this_times = decim_times[decim[ii]] | ||||||||||||
| this_data = ( | ||||||||||||
| this_offset - self.mne.onionskin_data[ii] * self.mne.scale_factor | ||||||||||||
| ) | ||||||||||||
| this_data = this_data[..., :: decim[ii]] | ||||||||||||
| clip = 0.2 if self.mne.butterfly else 0.5 | ||||||||||||
| bottom = max(this_offset - clip, ylim[1]) | ||||||||||||
| height = min(2 * clip, ylim[0] - bottom) | ||||||||||||
| rect = Rectangle( | ||||||||||||
| xy=np.array([time_range[0], bottom]), | ||||||||||||
| width=time_range[1] - time_range[0], | ||||||||||||
| height=height, | ||||||||||||
| transform=self.mne.ax_main.transData, | ||||||||||||
| ) | ||||||||||||
| line.set_clip_path(rect) | ||||||||||||
| line.set_xdata(this_times) | ||||||||||||
| line.set_ydata(this_data) | ||||||||||||
| color = ch_colors[ii] | ||||||||||||
| line.set_color(color) | ||||||||||||
| line.set_zorder(self.mne.zorder["data"] - 1) | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| subjects = [1] | ||||||||||||
| runs = [1, 2] | ||||||||||||
| raw_fnames = eegbci.load_data(subjects, runs) | ||||||||||||
| _set_onionskin_eeg(read_raw_edf(raw_fnames[0], preload=True)) | ||||||||||||
| read_raw_edf(raw_fnames[1], preload=True).plot( | ||||||||||||
| block=True, figure_class=OnionskinMNEBrowseFigure | ||||||||||||
| ) | ||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1714,6 +1714,14 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): | |
| and if absent, falls back to ``'estimated'``. | ||
| """ | ||
|
|
||
| docdict["figure_class"] = """ | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is probably failing due to so needs to move down two more entries |
||
| figure_class : class | ||
| The backend specific `MNEBrowseFigure` class to use. This is typically used | ||
| to pass a subclass in order to customize the plot. This parameter requires | ||
| cooperation from the backend, and is currently only supported by the | ||
| ``matplotlib`` backend. | ||
| """ | ||
|
|
||
| docdict["fig_background"] = """ | ||
| fig_background : None | array | ||
| A background image for the figure. This must be a valid input to | ||
|
|
||

There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
single backtick is default linking mode (which for us is
:py:obj:I think), if not adding to public API need code mode