Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/changes/dev/14018.newfeature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add a ``show_zero_line`` parameter (and :kbd:`0` keyboard shortcut) to :func:`mne.io.Raw.plot` and :func:`mne.Epochs.plot` to show a reference line at each channel's zero, by `Clemens Brunner`_.
2 changes: 2 additions & 0 deletions mne/epochs.py
Original file line number Diff line number Diff line change
Expand Up @@ -1317,6 +1317,7 @@ def plot(
butterfly=False,
show_scrollbars=True,
show_scalebars=True,
show_zero_line=False,
epoch_colors=None,
event_id=None,
group_by="type",
Expand Down Expand Up @@ -1345,6 +1346,7 @@ def plot(
butterfly=butterfly,
show_scrollbars=show_scrollbars,
show_scalebars=show_scalebars,
show_zero_line=show_zero_line,
epoch_colors=epoch_colors,
event_id=event_id,
group_by=group_by,
Expand Down
2 changes: 2 additions & 0 deletions mne/io/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1993,6 +1993,7 @@ def plot(
event_id=None,
show_scrollbars=True,
show_scalebars=True,
show_zero_line=False,
time_format="float",
precompute=None,
use_opengl=None,
Expand Down Expand Up @@ -2034,6 +2035,7 @@ def plot(
event_id=event_id,
show_scrollbars=show_scrollbars,
show_scalebars=show_scalebars,
show_zero_line=show_zero_line,
time_format=time_format,
precompute=precompute,
use_opengl=use_opengl,
Expand Down
11 changes: 11 additions & 0 deletions mne/utils/docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -4271,6 +4271,17 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75):
.. versionadded:: 0.20.0
"""

docdict["show_zero_line"] = """
show_zero_line : bool
Whether to show the zero line for each channel trace when the plot is
initialized. The zero line marks the true zero of each channel's own
displayed trace, independent of any DC removal or highpass filtering
already applied to the data. Can be toggled after initialization by
Comment on lines +4277 to +4279

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm somehow it's hard for me to parse this. I expected at first that hitting "d" would change the position of the line in the plot. I think calling it "true zero" made me think of "where zero actually is for the signal". So maybe

Suggested change
initialized. The zero line marks the true zero of each channel's own
displayed trace, independent of any DC removal or highpass filtering
already applied to the data. Can be toggled after initialization by
initialized. The zero line marks the centering point of each channel's
**displayed trace**, independent of any DC removal or highpass filtering
already applied to the data. Can be toggled after initialization by

I wonder if there is value in having a third mode where toggling DC removal changes where the "zero line" is displayed so that it's actually at where the zero level is for that channel (in other words, treating "DC removal" as more of a visual vertical translation rather than a subtraction of any values from the trace)

pressing :kbd:`0` while the plot window is focused. Default is ``False``.

.. versionadded:: 1.13
"""

docdict["size_topomap"] = """
size : float
Side length of each subplot in inches.
Expand Down
2 changes: 2 additions & 0 deletions mne/viz/_figure.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ def __init__(self, **kwargs):
patch=0,
grid=1,
ann=2,
zero_line=3,
events=10003,
bads=10004,
data=10005,
Expand Down Expand Up @@ -140,6 +141,7 @@ def __init__(self, **kwargs):
self.mne.scale_factor = 0.5 if self.mne.butterfly else 1.0
self.mne.scalebars = dict()
self.mne.scalebar_texts = dict()
self.mne.zero_lines = list()
# ancillary child figures
self.mne.child_figs = list()
self.mne.fig_help = None
Expand Down
35 changes: 35 additions & 0 deletions mne/viz/_mpl_figure.py
Original file line number Diff line number Diff line change
Expand Up @@ -645,6 +645,13 @@ def __init__(self, inst, figsize, ica=None, xlabel="Time (s)", **kwargs):
self.mne.traces = ax_main.plot(
np.full((1, self.mne.n_channels), np.nan), **self.mne.trace_kwargs
)
self.mne.zero_line_kwargs = dict(
color=self.mne.fgcolor,
alpha=0.5,
linewidth=0.5,
linestyle="dashed",
zorder=self.mne.zorder["zero_line"],
)

# SAVE UI ELEMENT HANDLES
vars(self.mne).update(
Expand Down Expand Up @@ -874,6 +881,8 @@ def _keypress(self, event):
checkbox.set_active(0)
elif key == "s": # scalebars
self._toggle_scalebars(event)
elif key == "0": # zero line
self._toggle_zero_line(event)
elif key == "w": # toggle noise cov whitening
self._toggle_whitening()
elif key == "z": # zen mode: hide scrollbars and buttons
Expand Down Expand Up @@ -1124,6 +1133,7 @@ def _get_help_text(self):
("shift+j", "Toggle all SSPs"),
("p", "Toggle draggable annotations" if is_raw else None),
("s", "Toggle scalebars" if not is_ica else None),
("0", "Toggle zero line"),
("z", "Toggle scrollbars"),
("t", "Toggle time format" if not is_epo else None),
("F11", "Toggle fullscreen" if not is_mac else None),
Expand Down Expand Up @@ -1993,6 +2003,11 @@ def _toggle_scalebars(self, event):
self.mne.scalebars_visible = not self.mne.scalebars_visible
self._redraw(update_data=False)

def _toggle_zero_line(self, event):
"""Show/hide the zero line for each channel trace."""
self.mne.zero_line_visible = not self.mne.zero_line_visible
self._redraw(update_data=False)

def _draw_one_scalebar(self, x, y, ch_type):
"""Draw a scalebar."""
from .utils import _simplify_float
Expand Down Expand Up @@ -2213,6 +2228,23 @@ def _draw_traces(self):
trace.remove()
self.mne.traces = self.mne.traces[:n_picks]

# add/remove zero lines if needed
if self.mne.zero_line_visible:
if n_picks > len(self.mne.zero_lines):
n_new_chs = n_picks - len(self.mne.zero_lines)
new_zero_lines = self.mne.ax_main.plot(
np.full((1, n_new_chs), np.nan), **self.mne.zero_line_kwargs
)
self.mne.zero_lines.extend(new_zero_lines)
extra_zero_lines = self.mne.zero_lines[n_picks:]
for zero_line in extra_zero_lines:
zero_line.remove()
self.mne.zero_lines = self.mne.zero_lines[:n_picks]
elif self.mne.zero_lines:
for zero_line in self.mne.zero_lines:
zero_line.remove()
self.mne.zero_lines = list()

# check for bad epochs
time_range = (self.mne.times + self.mne.first_time)[[0, -1]]
if self.mne.instance_type == "epochs":
Expand Down Expand Up @@ -2248,6 +2280,9 @@ def _draw_traces(self):
this_name = ch_names[ii]
this_type = ch_types[ii]
this_offset = offsets[ii]
if self.mne.zero_line_visible:
self.mne.zero_lines[ii].set_xdata(time_range)
self.mne.zero_lines[ii].set_ydata((this_offset, this_offset))
this_times = decim_times[decim[ii]]
this_data = this_offset - self.mne.data[ii] * self.mne.scale_factor
this_data = this_data[..., :: decim[ii]]
Expand Down
3 changes: 3 additions & 0 deletions mne/viz/epochs.py
Original file line number Diff line number Diff line change
Expand Up @@ -755,6 +755,7 @@ def plot_epochs(
butterfly=False,
show_scrollbars=True,
show_scalebars=True,
show_zero_line=False,
epoch_colors=None,
event_id=None,
group_by="type",
Expand Down Expand Up @@ -845,6 +846,7 @@ def plot_epochs(
%(show_scalebars)s

.. versionadded:: 0.24.0
%(show_zero_line)s
epoch_colors : list of (n_epochs) list (of n_channels) | None
Colors to use for individual epochs. If None, use default colors.
event_id : bool | dict
Expand Down Expand Up @@ -1081,6 +1083,7 @@ def plot_epochs(
clipping=None,
scrollbars_visible=show_scrollbars,
scalebars_visible=show_scalebars,
zero_line_visible=show_zero_line,
window_title=title,
xlabel="Epoch number",
# pyqtgraph-specific
Expand Down
1 change: 1 addition & 0 deletions mne/viz/ica.py
Original file line number Diff line number Diff line change
Expand Up @@ -1442,6 +1442,7 @@ def _plot_sources(
clipping=None,
scrollbars_visible=show_scrollbars,
scalebars_visible=False,
zero_line_visible=False,
window_title=title,
precompute=precompute,
use_opengl=use_opengl,
Expand Down
3 changes: 3 additions & 0 deletions mne/viz/raw.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ def plot_raw(
event_id=None,
show_scrollbars=True,
show_scalebars=True,
show_zero_line=False,
time_format="float",
precompute=None,
use_opengl=None,
Expand Down Expand Up @@ -213,6 +214,7 @@ def plot_raw(
%(show_scalebars)s

.. versionadded:: 0.20.0
%(show_zero_line)s
%(time_format)s
%(precompute)s
%(use_opengl)s
Expand Down Expand Up @@ -427,6 +429,7 @@ def plot_raw(
clipping=clipping,
scrollbars_visible=show_scrollbars,
scalebars_visible=show_scalebars,
zero_line_visible=show_zero_line,
window_title=title,
bgcolor=bgcolor,
# Qt-specific
Expand Down
16 changes: 16 additions & 0 deletions mne/viz/tests/test_raw.py
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,22 @@ def test_scale_bar(browser_backend):
browser_backend._close_all()


def test_zero_line(raw, mpl_backend):
"""Test toggling the zero line for raw (matplotlib backend only)."""
fig = raw.plot()
assert not fig.mne.zero_line_visible
assert len(fig.mne.zero_lines) == 0
fig._fake_keypress("0")
assert fig.mne.zero_line_visible
assert len(fig.mne.zero_lines) == len(fig.mne.picks)
for zero_line, offset in zip(fig.mne.zero_lines, fig.mne.trace_offsets):
assert_allclose(zero_line.get_ydata(), (offset, offset))
assert_allclose(fig.mne.zero_lines[0].get_xdata(), fig.mne.ax_main.get_xlim())
fig._fake_keypress("0") # toggle back off -> artists removed
assert not fig.mne.zero_line_visible
assert len(fig.mne.zero_lines) == 0


def test_plot_raw_selection(raw, browser_backend):
"""Test selection mode of plot_raw()."""
ismpl = browser_backend.name == "matplotlib"
Expand Down
Loading