Skip to content
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

MRG: Add "highlight" parameter to Evoked.plot() to conveniently highlight time periods #10614

Merged
merged 4 commits into from
May 8, 2022
Merged
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
2 changes: 2 additions & 0 deletions doc/changes/latest.inc
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ Enhancements

- :func:`mne.viz.plot_evoked_topomap` and :meth:`mne.Evoked.plot_topomap` can now average the topographic maps across different time periods for each time point. To do this, pass a list of periods via the ``average`` parameter (:gh:`10610` by `Richard Höchenberger`_)

- :func:`mne.viz.plot_evoked` and :meth:`mne.Evoked.plot` gained a new parameter, ``highlight``, to visually highlight time periods of interest (:gh:`10614` by `Richard Höchenberger`_)

Bugs
~~~~
- Make ``color`` parameter check in in :func:`mne.viz.plot_evoked_topo` consistent (:gh:`10217` by :newcontrib:`T. Wang` and `Stefan Appelhoff`_)
Expand Down
6 changes: 4 additions & 2 deletions mne/evoked.py
Original file line number Diff line number Diff line change
Expand Up @@ -430,14 +430,16 @@ def plot(self, picks=None, exclude='bads', unit=True, show=True, ylim=None,
xlim='tight', proj=False, hline=None, units=None, scalings=None,
titles=None, axes=None, gfp=False, window_title=None,
spatial_colors=False, zorder='unsorted', selectable=True,
noise_cov=None, time_unit='s', sphere=None, verbose=None):
noise_cov=None, time_unit='s', sphere=None, *, highlight=None,
verbose=None):
return plot_evoked(
self, picks=picks, exclude=exclude, unit=unit, show=show,
ylim=ylim, proj=proj, xlim=xlim, hline=hline, units=units,
scalings=scalings, titles=titles, axes=axes, gfp=gfp,
window_title=window_title, spatial_colors=spatial_colors,
zorder=zorder, selectable=selectable, noise_cov=noise_cov,
time_unit=time_unit, sphere=sphere, verbose=verbose)
time_unit=time_unit, sphere=sphere, highlight=highlight,
verbose=verbose)

@copy_function_doc_to_method_doc(plot_evoked_image)
def plot_image(self, picks=None, exclude='bads', unit=True, show=True,
Expand Down
49 changes: 43 additions & 6 deletions mne/viz/evoked.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,8 @@ def _plot_evoked(evoked, picks, exclude, unit, show, ylim, proj, xlim, hline,
selectable=True, zorder='unsorted',
noise_cov=None, colorbar=True, mask=None, mask_style=None,
mask_cmap=None, mask_alpha=.25, time_unit='s',
show_names=False, group_by=None, sphere=None):
show_names=False, group_by=None, sphere=None, *,
highlight=None):
"""Aux function for plot_evoked and plot_evoked_image (cf. docstrings).

Extra param is:
Expand Down Expand Up @@ -273,6 +274,15 @@ def _plot_evoked(evoked, picks, exclude, unit, show, ylim, proj, xlim, hline,

_check_option('gfp', gfp, [True, False, 'only'])

if highlight is not None:
highlight = np.array(highlight, dtype=float)
highlight = np.atleast_2d(highlight)
if highlight.shape[1] != 2:
raise ValueError(
f'"highlight" must be reshapable into a 2D array with shape '
f'(n, 2). Got {highlight.shape}.'
)

scalings = _handle_default('scalings', scalings)
titles = _handle_default('titles', titles)
units = _handle_default('units', units)
Expand Down Expand Up @@ -351,7 +361,7 @@ def _plot_evoked(evoked, picks, exclude, unit, show, ylim, proj, xlim, hline,
units, scalings, hline, gfp, types, zorder, xlim, ylim,
times, bad_ch_idx, titles, ch_types_used, selectable,
False, line_alpha=1., nave=evoked.nave,
time_unit=time_unit, sphere=sphere)
time_unit=time_unit, sphere=sphere, highlight=highlight)
plt.setp(axes, xlabel='Time (%s)' % time_unit)

elif plot_type == 'image':
Expand Down Expand Up @@ -382,7 +392,7 @@ def _plot_evoked(evoked, picks, exclude, unit, show, ylim, proj, xlim, hline,
def _plot_lines(data, info, picks, fig, axes, spatial_colors, unit, units,
scalings, hline, gfp, types, zorder, xlim, ylim, times,
bad_ch_idx, titles, ch_types_used, selectable, psd,
line_alpha, nave, time_unit, sphere):
line_alpha, nave, time_unit, sphere, *, highlight):
"""Plot data as butterfly plot."""
from matplotlib import patheffects, pyplot as plt
from matplotlib.widgets import SpanSelector
Expand Down Expand Up @@ -478,6 +488,7 @@ def _plot_lines(data, info, picks, fig, axes, spatial_colors, unit, units,
linewidth=0.5)[0])
line_list[-1].set_pickradius(3.)

# Plot GFP / RMS
if gfp:
if gfp in [True, 'only']:
if this_type == 'eeg':
Expand Down Expand Up @@ -530,7 +541,21 @@ def _plot_lines(data, info, picks, fig, axes, spatial_colors, unit, units,
for h in hline:
c = ('grey' if spatial_colors is True else 'r')
ax.axhline(h, linestyle='--', linewidth=2, color=c)

# Plot highlights
if highlight is not None:
this_ylim = ax.get_ylim() if (ylim is None or this_type not in
ylim.keys()) else ylim[this_type]
for this_highlight in highlight:
ax.fill_betweenx(
this_ylim, this_highlight[0], this_highlight[1],
facecolor='orange', alpha=0.15, zorder=99
)
# Put back the y limits as fill_betweenx messes them up
ax.set_ylim(this_ylim)

lines.append(line_list)

if selectable:
for ax in np.array(axes)[selectables]:
if len(ax.lines) == 1:
Expand Down Expand Up @@ -643,7 +668,7 @@ def plot_evoked(evoked, picks=None, exclude='bads', unit=True, show=True,
scalings=None, titles=None, axes=None, gfp=False,
window_title=None, spatial_colors=False, zorder='unsorted',
selectable=True, noise_cov=None, time_unit='s', sphere=None,
verbose=None):
*, highlight=None, verbose=None):
"""Plot evoked data using butterfly plots.

Left click to a line shows the channel name. Selecting an area by clicking
Expand Down Expand Up @@ -749,6 +774,18 @@ def plot_evoked(evoked, picks=None, exclude='bads', unit=True, show=True,

.. versionadded:: 0.16
%(sphere_topomap_auto)s
highlight : array-like of float, shape(2,) | array-like of float, shape (n, 2) | None
Segments of the data to highlight by means of a light-yellow
background color. Can be used to put visual emphasis on certain
time periods. The time periods must be specified as ``array-like``
objects in the form of ``(t_start, t_end)`` in the unit given by the
``time_unit`` parameter.
Multiple time periods can be specified by passing an ``array-like``
object of individual time periods (e.g., for 3 time periods, the shape
of the passed object would be ``(3, 2)``. If ``None``, no highlighting
is applied.

.. versionadded:: 1.1
%(verbose)s

Returns
Expand All @@ -759,14 +796,14 @@ def plot_evoked(evoked, picks=None, exclude='bads', unit=True, show=True,
See Also
--------
mne.viz.plot_evoked_white
"""
""" # noqa: E501
return _plot_evoked(
evoked=evoked, picks=picks, exclude=exclude, unit=unit, show=show,
ylim=ylim, proj=proj, xlim=xlim, hline=hline, units=units,
scalings=scalings, titles=titles, axes=axes, plot_type="butterfly",
gfp=gfp, window_title=window_title, spatial_colors=spatial_colors,
selectable=selectable, zorder=zorder, noise_cov=noise_cov,
time_unit=time_unit, sphere=sphere)
time_unit=time_unit, sphere=sphere, highlight=highlight)


def plot_evoked_topo(evoked, layout=None, layout_scale=0.945,
Expand Down
14 changes: 14 additions & 0 deletions mne/viz/tests/test_evoked.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,20 @@ def test_plot_evoked():
evoked.plot(verbose=True, time_unit='s')
assert 'Need more than one' in log_file.getvalue()

# Test highlight
for highlight in [
(0, 0.1),
[(0, 0.1), (0.1, 0.2)]
]:
fig = evoked.plot(time_unit='s', highlight=highlight)
for ax in fig.get_axes():
highlighted_areas = [child for child in ax.get_children()
if isinstance(child, PolyCollection)]
assert len(highlighted_areas) == len(np.atleast_2d(highlight))

with pytest.raises(ValueError, match='must be reshapable into a 2D array'):
fig = evoked.plot(time_unit='s', highlight=0.1)


def test_constrained_layout():
"""Test that we handle constrained layouts correctly."""
Expand Down
2 changes: 1 addition & 1 deletion mne/viz/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2233,7 +2233,7 @@ def _plot_psd(inst, fig, freqs, psd_list, picks_list, titles_list,
ylim=None, times=freqs, bad_ch_idx=[], titles=titles,
ch_types_used=ch_types_used, selectable=True, psd=True,
line_alpha=line_alpha, nave=None, time_unit='ms',
sphere=sphere)
sphere=sphere, highlight=None)

for ii, (ax, xlabel) in enumerate(zip(ax_list, xlabels_list)):
ax.grid(True, linestyle=':')
Expand Down
12 changes: 12 additions & 0 deletions tutorials/evoked/20_visualize_evoked.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,18 @@

evks['aud/left'].plot(picks='mag', spatial_colors=True, gfp=True)

# %%
# Interesting time periods can be highlighted via the ``highlight`` parameter.

time_ranges_of_interest = [
(0.05, 0.14),
(0.22, 0.27)
]
evks['aud/left'].plot(
picks='mag', spatial_colors=True, gfp=True,
highlight=time_ranges_of_interest
)

# %%
# Plotting scalp topographies
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^
Expand Down