From cdad29dac30041ee79d4e719cb53ad63ffdf21a9 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Thu, 26 Oct 2023 21:51:29 -0400 Subject: [PATCH] ENH: set color for bad channel with spatial_colors=True (#12142) --- doc/changes/devel.rst | 2 ++ mne/time_frequency/tests/test_spectrum.py | 14 +++++++++++--- mne/viz/evoked.py | 8 +++++--- mne/viz/tests/test_evoked.py | 7 +++++++ mne/viz/utils.py | 3 ++- tutorials/intro/70_report.py | 3 +-- 6 files changed, 28 insertions(+), 9 deletions(-) diff --git a/doc/changes/devel.rst b/doc/changes/devel.rst index 5457e844f6c..c52705a9a9d 100644 --- a/doc/changes/devel.rst +++ b/doc/changes/devel.rst @@ -38,6 +38,7 @@ Enhancements - Add support for writing forward solutions to HDF5 and convenience function :meth:`mne.Forward.save` (:gh:`12036` by `Eric Larson`_) - Refactored internals of :func:`mne.read_annotations` (:gh:`11964` by `Paul Roujansky`_) - Add support for drawing MEG sensors in :ref:`mne coreg` (:gh:`12098` by `Eric Larson`_) +- Bad channels are now colored gray in addition to being dashed when spatial colors are used in :func:`mne.viz.plot_evoked` and related functions (:gh:`12142` by `Eric Larson`_) - By default MNE-Python creates matplotlib figures with ``layout='constrained'`` rather than the default ``layout='tight'`` (:gh:`12050`, :gh:`12103` by `Mathieu Scheltienne`_ and `Eric Larson`_) - Enhance :func:`~mne.viz.plot_evoked_field` with a GUI that has controls for time, colormap, and contour lines (:gh:`11942` by `Marijn van Vliet`_) - Add :class:`mne.viz.ui_events.UIEvent` linking for interactive colorbars, allowing users to link figures and change the colormap and limits interactively. This supports :func:`~mne.viz.plot_evoked_topomap`, :func:`~mne.viz.plot_ica_components`, :func:`~mne.viz.plot_tfr_topomap`, :func:`~mne.viz.plot_projs_topomap`, :meth:`~mne.Evoked.plot_image`, and :meth:`~mne.Epochs.plot_image` (:gh:`12057` by `Santeri Ruuskanen`_) @@ -71,6 +72,7 @@ Bugs - Fix :func:`~mne.viz.plot_volume_source_estimates` with :class:`~mne.VolSourceEstimate` which include a list of vertices (:gh:`12025` by `Mathieu Scheltienne`_) - Add support for non-ASCII characters in Annotations, Evoked comments, etc when saving to FIFF format (:gh:`12080` by `Daniel McCloy`_) - Correctly handle passing ``"eyegaze"`` or ``"pupil"`` to :meth:`mne.io.Raw.pick` (:gh:`12019` by `Scott Huberty`_) +- Fix bug with :func:`mne.time_frequency.Spectrum.plot` and related functions where bad channels were not marked (:gh:`12142` by `Eric Larson`_) - Fix bug with :func:`~mne.viz.plot_raw` where changing ``MNE_BROWSER_BACKEND`` via :func:`~mne.set_config` would have no effect within a Python session (:gh:`12078` by `Santeri Ruuskanen`_) - Improve handling of ``method`` argument in the channel interpolation function to support :class:`str` and raise helpful error messages (:gh:`12113` by `Mathieu Scheltienne`_) - Fix combination of ``DIN`` event channels into a single synthetic trigger channel ``STI 014`` by the MFF reader of :func:`mne.io.read_raw_egi` (:gh:`12122` by `Mathieu Scheltienne`_) diff --git a/mne/time_frequency/tests/test_spectrum.py b/mne/time_frequency/tests/test_spectrum.py index 7aaa5b40ea6..96fe89a2e6d 100644 --- a/mne/time_frequency/tests/test_spectrum.py +++ b/mne/time_frequency/tests/test_spectrum.py @@ -1,9 +1,9 @@ from contextlib import nullcontext from functools import partial -import matplotlib.pyplot as plt import numpy as np import pytest +from matplotlib.colors import same_color from numpy.testing import assert_allclose, assert_array_equal from mne import Annotations, create_info, make_fixed_length_epochs @@ -449,8 +449,16 @@ def test_plot_spectrum(kind, array, request): data, freqs = spectrum.get_data(return_freqs=True) Klass = SpectrumArray if kind == "raw" else EpochsSpectrumArray spectrum = Klass(data=data, info=spectrum.info, freqs=freqs) + spectrum.info["bads"] = spectrum.ch_names[:1] # one grad channel spectrum.plot(average=True, amplitude=True, spatial_colors=True) - spectrum.plot(average=False, amplitude=False, spatial_colors=False) + spectrum.plot(average=True, amplitude=False, spatial_colors=False) + n_grad = sum(ch_type == "grad" for ch_type in spectrum.get_channel_types()) + for amp, sc in ((True, True), (False, False)): + fig = spectrum.plot(average=False, amplitude=amp, spatial_colors=sc, exclude=()) + lines = fig.axes[0].lines[2:] # grads, ignore two vlines + assert len(lines) == n_grad + bad_color = "0.5" if sc else "r" + n_bad = sum(same_color(line.get_color(), bad_color) for line in lines) + assert n_bad == 1 spectrum.plot_topo() spectrum.plot_topomap() - plt.close("all") diff --git a/mne/viz/evoked.py b/mne/viz/evoked.py index 6abcbcc0d1d..1c6712a6bec 100644 --- a/mne/viz/evoked.py +++ b/mne/viz/evoked.py @@ -679,15 +679,17 @@ def _plot_lines( _handle_spatial_colors( colors, info, idx, this_type, psd, ax, sphere ) + bad_color = (0.5, 0.5, 0.5) else: if isinstance(_spat_col, (tuple, str)): col = [_spat_col] else: col = ["k"] + bad_color = "r" colors = col * len(idx) - for i in bad_ch_idx: - if i in idx: - colors[idx.index(i)] = "r" + for i in bad_ch_idx: + if i in idx: + colors[idx.index(i)] = bad_color if zorder == "std": # find the channels with the least activity diff --git a/mne/viz/tests/test_evoked.py b/mne/viz/tests/test_evoked.py index c089b064d4a..51b83f222fa 100644 --- a/mne/viz/tests/test_evoked.py +++ b/mne/viz/tests/test_evoked.py @@ -16,6 +16,7 @@ import pytest from matplotlib import gridspec from matplotlib.collections import PolyCollection +from matplotlib.colors import same_color from mpl_toolkits.axes_grid1.parasite_axes import HostAxes # spatial_colors from numpy.testing import assert_allclose @@ -134,6 +135,12 @@ def test_plot_evoked(): amplitudes = _get_amplitudes(fig) assert len(amplitudes) == len(default_picks) assert evoked.proj is False + assert evoked.info["bads"] == ["MEG 2641", "EEG 004"] + eeg_lines = fig.axes[2].lines + n_eeg = sum(ch_type == "eeg" for ch_type in evoked.get_channel_types()) + assert len(eeg_lines) == n_eeg == 4 + n_bad = sum(same_color(line.get_color(), "0.5") for line in eeg_lines) + assert n_bad == 1 # Test a click ax = fig.get_axes()[0] line = ax.lines[0] diff --git a/mne/viz/utils.py b/mne/viz/utils.py index c81bdf354c2..e9c36281bae 100644 --- a/mne/viz/utils.py +++ b/mne/viz/utils.py @@ -2506,6 +2506,7 @@ def _plot_psd( if not average: picks = np.concatenate(picks_list) info = pick_info(inst.info, sel=picks, copy=True) + bad_ch_idx = [info["ch_names"].index(ch) for ch in info["bads"]] types = np.array(info.get_channel_types()) ch_types_used = list() for this_type in _VALID_CHANNEL_TYPES: @@ -2538,7 +2539,7 @@ def _plot_psd( xlim=(freqs[0], freqs[-1]), ylim=None, times=freqs, - bad_ch_idx=[], + bad_ch_idx=bad_ch_idx, titles=titles, ch_types_used=ch_types_used, selectable=True, diff --git a/tutorials/intro/70_report.py b/tutorials/intro/70_report.py index a7d3b02b2b3..951c82a5e6a 100644 --- a/tutorials/intro/70_report.py +++ b/tutorials/intro/70_report.py @@ -17,8 +17,7 @@ HTML pages it generates are self-contained and do not require a running Python environment. However, it is less flexible as you can't change code and re-run something directly within the browser. This tutorial covers the basics of -building a report. As usual, we will start by importing the modules and data we -need: +building a report. As usual, we will start by importing the modules and data we need: """ # %%