Skip to content

Commit ab77d2c

Browse files
frankierautofix-ci[bot]larsoner
authored
Allow subclasses of FigureClass to be passed to plot_raw/plot_epochs (#13979)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Eric Larson <larson.eric.d@gmail.com>
1 parent 997490f commit ab77d2c

10 files changed

Lines changed: 191 additions & 1 deletion

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
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`

doc/changes/names.inc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@
114114
.. _Felix Raimundo: https://github.com/gamazeps
115115
.. _Florian Hofer: https://github.com/hofaflo
116116
.. _Florin Pop: https://github.com/florin-pop
117+
.. _Frankie Robertson: https://github.com/frankier
117118
.. _Frederik Weber: https://github.com/Frederik-D-Weber
118119
.. _Fu-Te Wong: https://github.com/zuxfoucault
119120
.. _Gennadiy Belonosov: https://github.com/Genuster

doc/conf.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,7 @@
323323
"and",
324324
"as",
325325
"between",
326+
"class",
326327
"data",
327328
"instance",
328329
"instances",
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
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+
)

mne/epochs.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1327,6 +1327,7 @@ def plot(
13271327
overview_mode=None,
13281328
splash=True,
13291329
annotation_colors=None,
1330+
figure_class=None,
13301331
):
13311332
return plot_epochs(
13321333
self,
@@ -1354,6 +1355,7 @@ def plot(
13541355
overview_mode=overview_mode,
13551356
splash=splash,
13561357
annotation_colors=annotation_colors,
1358+
figure_class=figure_class,
13571359
)
13581360

13591361
@copy_function_doc_to_method_doc(plot_topo_image_epochs)

mne/io/base.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2001,6 +2001,7 @@ def plot(
20012001
overview_mode=None,
20022002
splash=True,
20032003
verbose=None,
2004+
figure_class=None,
20042005
):
20052006
return plot_raw(
20062007
self,
@@ -2042,6 +2043,7 @@ def plot(
20422043
overview_mode=overview_mode,
20432044
splash=splash,
20442045
verbose=verbose,
2046+
figure_class=figure_class,
20452047
)
20462048

20472049
@property

mne/utils/docs.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1721,6 +1721,14 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75):
17211721
A matplotlib-compatible color to use for the figure background. Defaults to black.
17221722
"""
17231723

1724+
docdict["figure_class"] = """
1725+
figure_class : class
1726+
The backend specific ``MNEBrowseFigure`` class to use. This is typically used
1727+
to pass a subclass in order to customize the plot. This parameter requires
1728+
cooperation from the backend, and is currently only supported by the
1729+
``matplotlib`` backend.
1730+
"""
1731+
17241732
docdict["filter_length"] = """
17251733
filter_length : str | int
17261734
Length of the FIR filter to use (if applicable):

mne/viz/_mpl_figure.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2638,7 +2638,11 @@ def _init_browser(**kwargs):
26382638
"""Instantiate a new MNE browse-style figure."""
26392639
from mne.io import BaseRaw
26402640

2641-
fig = _figure(toolbar=False, FigureClass=MNEBrowseFigure, layout=None, **kwargs)
2641+
figure_class = kwargs.pop("figure_class", None)
2642+
if figure_class is None:
2643+
figure_class = MNEBrowseFigure
2644+
2645+
fig = _figure(toolbar=False, FigureClass=figure_class, layout=None, **kwargs)
26422646

26432647
# splash is ignored (maybe we could do it for mpl if we get_backend() and
26442648
# check if it's Qt... but seems overkill)

mne/viz/epochs.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -765,6 +765,7 @@ def plot_epochs(
765765
overview_mode=None,
766766
splash=True,
767767
annotation_colors=None,
768+
figure_class=None,
768769
):
769770
"""Visualize epochs.
770771
@@ -875,6 +876,9 @@ def plot_epochs(
875876
will trigger a warning. If ``None`` (default), automatic colors are used.
876877
877878
.. versionadded:: 1.12.1
879+
%(figure_class)s
880+
881+
.. versionadded:: 1.13
878882
879883
Returns
880884
-------
@@ -1089,6 +1093,7 @@ def plot_epochs(
10891093
theme=theme,
10901094
overview_mode=overview_mode,
10911095
splash=splash,
1096+
figure_class=figure_class,
10921097
)
10931098

10941099
fig = _get_browser(show=show, block=block, **params)

mne/viz/raw.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ def plot_raw(
7575
overview_mode=None,
7676
splash=True,
7777
verbose=None,
78+
figure_class=None,
7879
):
7980
"""Plot raw data.
8081
@@ -227,6 +228,9 @@ def plot_raw(
227228
228229
.. versionadded:: 1.6
229230
%(verbose)s
231+
%(figure_class)s
232+
233+
.. versionadded:: 1.13
230234
231235
Returns
232236
-------
@@ -435,6 +439,7 @@ def plot_raw(
435439
theme=theme,
436440
overview_mode=overview_mode,
437441
splash=splash,
442+
figure_class=figure_class,
438443
)
439444

440445
fig = _get_browser(show=show, block=block, **params)

0 commit comments

Comments
 (0)