diff --git a/doc/changes/dev/13969.newfeature.rst b/doc/changes/dev/13969.newfeature.rst new file mode 100644 index 00000000000..81c936c27a6 --- /dev/null +++ b/doc/changes/dev/13969.newfeature.rst @@ -0,0 +1 @@ +Add ``mask_label_params`` parameter to :func:`~mne.viz.plot_topomap` and :func:`~mne.viz.plot_evoked_topomap` to allow for customisation of masked channel labels, by `Carina Forster`_. \ No newline at end of file diff --git a/mne/cov.py b/mne/cov.py index 1567b9b653b..70aaac3157f 100644 --- a/mne/cov.py +++ b/mne/cov.py @@ -327,6 +327,7 @@ def plot_topomap( show_names=False, mask=None, mask_params=None, + mask_label_params=None, contours=6, outlines="head", sphere=None, @@ -362,6 +363,9 @@ def plot_topomap( %(show_names_topomap)s %(mask_topomap)s %(mask_params_topomap)s + %(mask_label_params_topomap)s + + .. versionadded:: 1.13 %(contours_topomap)s %(outlines_topomap)s %(sphere_topomap_auto)s @@ -435,6 +439,7 @@ def plot_topomap( show_names=show_names, mask=mask, mask_params=mask_params, + mask_label_params=mask_label_params, outlines=outlines, contours=contours, image_interp=image_interp, diff --git a/mne/decoding/csp.py b/mne/decoding/csp.py index 56b66babaf7..18d8d5e0246 100644 --- a/mne/decoding/csp.py +++ b/mne/decoding/csp.py @@ -370,6 +370,7 @@ def plot_patterns( show_names=False, mask=None, mask_params=None, + mask_label_params=None, contours=6, outlines="head", sphere=None, @@ -409,6 +410,9 @@ def plot_patterns( %(show_names_topomap)s %(mask_patterns_topomap)s %(mask_params_topomap)s + %(mask_label_params_topomap)s + + .. versionadded:: 1.13 %(contours_topomap)s %(outlines_topomap)s %(sphere_topomap_auto)s @@ -453,6 +457,7 @@ def plot_patterns( show_names=show_names, mask=mask, mask_params=mask_params, + mask_label_params=mask_label_params, contours=contours, outlines=outlines, sphere=sphere, @@ -487,6 +492,7 @@ def plot_filters( show_names=False, mask=None, mask_params=None, + mask_label_params=None, contours=6, outlines="head", sphere=None, @@ -526,6 +532,9 @@ def plot_filters( %(show_names_topomap)s %(mask_patterns_topomap)s %(mask_params_topomap)s + %(mask_label_params_topomap)s + + .. versionadded:: 1.13 %(contours_topomap)s %(outlines_topomap)s %(sphere_topomap_auto)s @@ -570,6 +579,7 @@ def plot_filters( show_names=show_names, mask=mask, mask_params=mask_params, + mask_label_params=mask_label_params, contours=contours, outlines=outlines, sphere=sphere, diff --git a/mne/decoding/spatial_filter.py b/mne/decoding/spatial_filter.py index 169cca7d005..d679bf3cff9 100644 --- a/mne/decoding/spatial_filter.py +++ b/mne/decoding/spatial_filter.py @@ -26,6 +26,7 @@ def _plot_model( show_names=False, mask=None, mask_params=None, + mask_label_params=None, contours=6, outlines="head", sphere=None, @@ -63,6 +64,7 @@ def _plot_model( show_names=show_names, mask=mask, mask_params=mask_params, + mask_label_params=mask_label_params, contours=contours, outlines=outlines, sphere=sphere, @@ -394,6 +396,7 @@ def plot_filters( show_names=False, mask=None, mask_params=None, + mask_label_params=None, contours=6, outlines="head", sphere=None, @@ -433,6 +436,9 @@ def plot_filters( %(show_names_topomap)s %(mask_evoked_topomap)s %(mask_params_topomap)s + %(mask_label_params_topomap)s + + .. versionadded:: 1.13 %(contours_topomap)s %(outlines_topomap)s %(sphere_topomap_auto)s @@ -469,6 +475,7 @@ def plot_filters( show_names=show_names, mask=mask, mask_params=mask_params, + mask_label_params=mask_label_params, contours=contours, outlines=outlines, sphere=sphere, @@ -503,6 +510,7 @@ def plot_patterns( show_names=False, mask=None, mask_params=None, + mask_label_params=None, contours=6, outlines="head", sphere=None, @@ -542,6 +550,9 @@ def plot_patterns( %(show_names_topomap)s %(mask_evoked_topomap)s %(mask_params_topomap)s + %(mask_label_params_topomap)s + + .. versionadded:: 1.13 %(contours_topomap)s %(outlines_topomap)s %(sphere_topomap_auto)s @@ -578,6 +589,7 @@ def plot_patterns( show_names=show_names, mask=mask, mask_params=mask_params, + mask_label_params=mask_label_params, contours=contours, outlines=outlines, sphere=sphere, diff --git a/mne/defaults.py b/mne/defaults.py index 41fc8603778..2175abeeb74 100644 --- a/mne/defaults.py +++ b/mne/defaults.py @@ -252,6 +252,10 @@ markeredgewidth=1, markersize=4, ), + mask_label_params=dict( + fontsize="medium", # respects theme + fontweight="bold", + ), coreg=dict( mri_fid_opacity=1.0, dig_fid_opacity=1.0, diff --git a/mne/evoked.py b/mne/evoked.py index c1d62a1df52..50e4ff8cb46 100644 --- a/mne/evoked.py +++ b/mne/evoked.py @@ -655,6 +655,7 @@ def plot_topomap( show_names=False, mask=None, mask_params=None, + mask_label_params=None, contours=6, outlines="head", sphere=None, @@ -697,6 +698,7 @@ def plot_topomap( show_names=show_names, mask=mask, mask_params=mask_params, + mask_label_params=mask_label_params, outlines=outlines, contours=contours, image_interp=image_interp, @@ -804,6 +806,7 @@ def animate_topomap( show_names=False, mask=None, mask_params=None, + mask_label_params=None, contours=6, outlines="head", sphere=None, @@ -848,6 +851,9 @@ def animate_topomap( %(show_names_topomap)s %(mask_evoked_topomap)s %(mask_params_topomap)s + %(mask_label_params_topomap)s + + .. versionadded:: 1.13 %(contours_topomap)s %(outlines_topomap)s %(sphere_topomap_auto)s @@ -917,6 +923,7 @@ def animate_topomap( show_names=show_names, mask=mask, mask_params=mask_params, + mask_label_params=mask_label_params, contours=contours, outlines=outlines, sphere=sphere, diff --git a/mne/preprocessing/_regress.py b/mne/preprocessing/_regress.py index 6c866b6e421..cb5a1339aed 100644 --- a/mne/preprocessing/_regress.py +++ b/mne/preprocessing/_regress.py @@ -271,6 +271,7 @@ def plot( show_names=False, mask=None, mask_params=None, + mask_label_params=None, contours=6, outlines="head", sphere=None, @@ -295,6 +296,7 @@ def plot( show_names=show_names, mask=mask, mask_params=mask_params, + mask_label_params=mask_label_params, contours=contours, outlines=outlines, sphere=sphere, diff --git a/mne/time_frequency/spectrum.py b/mne/time_frequency/spectrum.py index 6591527a28b..dab12d02ee1 100644 --- a/mne/time_frequency/spectrum.py +++ b/mne/time_frequency/spectrum.py @@ -213,6 +213,7 @@ def plot_psd_topomap( show_names=False, mask=None, mask_params=None, + mask_label_params=None, contours=0, outlines="head", sphere=None, @@ -249,6 +250,9 @@ def plot_psd_topomap( %(show_names_topomap)s %(mask_evoked_topomap)s %(mask_params_topomap)s + %(mask_label_params_topomap)s + + .. versionadded:: 1.13 %(contours_topomap)s %(outlines_topomap)s %(sphere_topomap_auto)s @@ -789,6 +793,7 @@ def plot_topomap( show_names=False, mask=None, mask_params=None, + mask_label_params=None, contours=6, outlines="head", sphere=None, @@ -819,6 +824,9 @@ def plot_topomap( %(show_names_topomap)s %(mask_evoked_topomap)s %(mask_params_topomap)s + %(mask_label_params_topomap)s + + .. versionadded:: 1.13 %(contours_topomap)s %(outlines_topomap)s %(sphere_topomap_auto)s @@ -881,6 +889,7 @@ def plot_topomap( names=names, mask=mask, mask_params=mask_params, + mask_label_params=mask_label_params, contours=contours, outlines=outlines, sphere=sphere, diff --git a/mne/time_frequency/tfr.py b/mne/time_frequency/tfr.py index 24f13f925bf..dace3a5b14b 100644 --- a/mne/time_frequency/tfr.py +++ b/mne/time_frequency/tfr.py @@ -2613,6 +2613,7 @@ def plot_topomap( show_names=False, mask=None, mask_params=None, + mask_label_params=None, contours=6, outlines="head", sphere=None, @@ -2643,6 +2644,7 @@ def plot_topomap( show_names=show_names, mask=mask, mask_params=mask_params, + mask_label_params=mask_label_params, contours=contours, outlines=outlines, sphere=sphere, @@ -3589,6 +3591,7 @@ def plot_topomap( show_names=False, mask=None, mask_params=None, + mask_label_params=None, contours=6, outlines="head", sphere=None, @@ -3619,6 +3622,7 @@ def plot_topomap( show_names=show_names, mask=mask, mask_params=mask_params, + mask_label_params=mask_label_params, contours=contours, outlines=outlines, sphere=sphere, diff --git a/mne/utils/docs.py b/mne/utils/docs.py index 564bb4abb0d..4ab2354af58 100644 --- a/mne/utils/docs.py +++ b/mne/utils/docs.py @@ -2572,6 +2572,13 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): example=" (useful for, e.g. marking which channels at which times a " "statistical test of the data reaches significance)", ) +docdict["mask_label_params_topomap"] = """ +mask_label_params : dict | None + Additional plotting parameters for significant sensor labels. + Default (None) equals:: + + dict(fontsize='medium', fontweight='bold') +""" docdict["mask_params_topomap"] = """ mask_params : dict | None Additional plotting parameters for plotting significant sensors. diff --git a/mne/viz/tests/test_topomap.py b/mne/viz/tests/test_topomap.py index 21e6eaed72c..578f958b1ef 100644 --- a/mne/viz/tests/test_topomap.py +++ b/mne/viz/tests/test_topomap.py @@ -9,7 +9,7 @@ import matplotlib.pyplot as plt import numpy as np import pytest -from matplotlib.colors import PowerNorm, TwoSlopeNorm +from matplotlib.colors import PowerNorm, TwoSlopeNorm, to_rgba from matplotlib.patches import Circle from numpy.testing import assert_almost_equal, assert_array_equal, assert_equal @@ -1008,7 +1008,7 @@ def test_plot_ch_adjacency(): assert len(collections) == 2 # make sure the point is green - green = matplotlib.colors.to_rgba("tab:green") + green = to_rgba("tab:green") assert (collections[1].get_facecolor() == green).all() # make sure adjacency entry is modified after second click on another node @@ -1091,3 +1091,88 @@ def test_plot_topomap_info_names_ordering(ch_type): assert displayed_names == list(expected_names), ( f"Expected {list(expected_names)}, got {displayed_names}" ) + + +def test_plot_topomap_mask_label_params(): + """Test that mask_label_params styles the masked-sensor labels.""" + evoked = read_evokeds(evoked_fname)[0] + + significant = ( + "EEG 001", + "EEG 002", + "EEG 003", + "EEG 004", + "EEG 005", + "EEG 006", + "EEG 007", + "EEG 008", + ) + sig = np.isin(evoked.ch_names, significant) + mask = np.zeros(evoked.data.shape, dtype=bool) + mask[sig, :] = True + + # test non-default + mask_label_params = dict( + fontsize="medium", + color="green", + fontweight="bold", + bbox=dict(facecolor="red", alpha=0.3), + ) + + fig = evoked.plot_topomap( + times=0.1, + ch_type="eeg", + mask=mask, + show_names=True, + mask_label_params=mask_label_params, + show=False, + ) + + fig.canvas.draw() # important for the bbox patches + + sig_labels = [t for ax in fig.axes for t in ax.texts if t.get_text() in significant] + + assert sig_labels, "there are no masked-channel labels" + + # non masked channels should not be green + nonsig_labels = [ + t + for ax in fig.axes + for t in ax.texts + if t.get_text() in evoked.ch_names and t.get_text() not in significant + ] + + for ns in nonsig_labels: + assert to_rgba(ns.get_color()) != to_rgba("green") + + # loop over significant labels and check text attributes + for sl in sig_labels: + assert to_rgba(sl.get_color()) == to_rgba("green") + assert sl.get_fontweight() == "bold" + patch = sl.get_bbox_patch() + assert patch is not None + assert np.allclose(patch.get_facecolor()[:3], (1.0, 0.0, 0.0)) # red + + plt.close(fig) + + # test default mask labels + # should be dict(fontsize="medium", fontweight="bold") + fig = evoked.plot_topomap( + times=0.1, + ch_type="eeg", + mask=mask, + show_names=True, + mask_label_params=None, + show=False, + ) + + sig_labels = [t for ax in fig.axes for t in ax.texts if t.get_text() in significant] + + assert sig_labels, "there are no masked-channel labels" + + # masked labels should be bold and colored the same as unmasked labels + for sl in sig_labels: + assert sl.get_fontweight() == "bold" + assert to_rgba(sl.get_color()) == to_rgba(nonsig_labels[0].get_color()) + + plt.close(fig) diff --git a/mne/viz/topomap.py b/mne/viz/topomap.py index 1eb40d8d167..03ccc0efe9b 100644 --- a/mne/viz/topomap.py +++ b/mne/viz/topomap.py @@ -1017,6 +1017,7 @@ def plot_topomap( names=None, mask=None, mask_params=None, + mask_label_params=None, contours=6, outlines="head", sphere=None, @@ -1049,6 +1050,9 @@ def plot_topomap( %(names_topomap)s %(mask_topomap)s %(mask_params_topomap)s + %(mask_label_params_topomap)s + + .. versionadded:: 1.13 %(contours_topomap)s %(outlines_topomap)s %(sphere_topomap_auto)s @@ -1117,6 +1121,7 @@ def plot_topomap( names=names, mask=mask, mask_params=mask_params, + mask_label_params=mask_label_params, outlines=outlines, contours=contours, image_interp=image_interp, @@ -1274,6 +1279,7 @@ def _plot_topomap( names=None, mask=None, mask_params=None, + mask_label_params=None, contours=6, outlines="head", sphere=None, @@ -1386,6 +1392,8 @@ def _plot_topomap( if "zorder" not in mask_params: mask_params["zorder"] = _TOPOMAP_ZORDER["sensors"] + mask_label_params = _handle_default("mask_label_params", mask_label_params) + # find mask limits and setup interpolation extent, Xi, Yi, interp = _setup_interp( pos, res, image_interp, extrapolate, outlines, border @@ -1467,15 +1475,36 @@ def _plot_topomap( _draw_outlines(axes, outlines) if names is not None and sensors: - for _pos, _name in zip(pos, names): - axes.text( - _pos[0], - _pos[1], - _name, - horizontalalignment="center", - verticalalignment="center", - size="x-small", - ) + if mask is None: + for _pos, _name in zip(pos, names): + axes.text( + _pos[0], + _pos[1], + _name, + horizontalalignment="center", + verticalalignment="center", + size="x-small", + ) + else: + for i, (_pos, _name) in enumerate(zip(pos, names)): + if mask[i]: + axes.text( + _pos[0], + _pos[1], + _name, + horizontalalignment="center", + verticalalignment="center", + **mask_label_params, + ) + else: + axes.text( + _pos[0], + _pos[1], + _name, + horizontalalignment="center", + verticalalignment="center", + size="x-small", + ) if onselect is not None: lim = axes.dataLim @@ -1897,6 +1926,7 @@ def plot_tfr_topomap( show_names=False, mask=None, mask_params=None, + mask_label_params=None, contours=6, outlines="head", sphere=None, @@ -1947,6 +1977,9 @@ def plot_tfr_topomap( %(show_names_topomap)s %(mask_evoked_topomap)s %(mask_params_topomap)s + %(mask_label_params_topomap)s + + .. versionadded:: 1.13 %(contours_topomap)s %(outlines_topomap)s %(sphere_topomap_auto)s @@ -2073,6 +2106,7 @@ def plot_tfr_topomap( names=names, mask=mask, mask_params=mask_params, + mask_label_params=mask_label_params, contours=contours, outlines=outlines, sphere=sphere, @@ -2125,6 +2159,7 @@ def plot_evoked_topomap( show_names=False, mask=None, mask_params=None, + mask_label_params=None, contours=6, outlines="head", sphere=None, @@ -2168,6 +2203,9 @@ def plot_evoked_topomap( %(show_names_topomap)s %(mask_evoked_topomap)s %(mask_params_topomap)s + %(mask_label_params_topomap)s + + .. versionadded:: 1.13 %(contours_topomap)s %(outlines_topomap)s %(sphere_topomap_auto)s @@ -2257,6 +2295,7 @@ def plot_evoked_topomap( show_names=show_names, mask=mask, mask_params=mask_params, + mask_label_params=mask_label_params, contours=contours, outlines=outlines, sphere=sphere, @@ -2297,6 +2336,7 @@ def _plot_evoked_topomap( show_names, mask, mask_params, + mask_label_params, contours, outlines, sphere, @@ -2339,6 +2379,7 @@ def _plot_evoked_topomap( del time_unit # mask_params defaults mask_params = _handle_default("mask_params", mask_params) + mask_label_params = _handle_default("mask_label_params", mask_label_params) mask_params["markersize"] *= size / 2.0 mask_params["markeredgewidth"] *= size / 2.0 # setup various parameters, and prepare outlines @@ -2529,6 +2570,7 @@ def _plot_evoked_topomap( cmap=cmap[0], cnorm=cnorm, mask_params=mask_params, + mask_label_params=mask_label_params, outlines=outlines, contours=contours, image_interp=image_interp, @@ -2746,6 +2788,7 @@ def _plot_topomap_multi_cbar( names, mask, mask_params, + mask_label_params, contours, image_interp, extrapolate, @@ -2775,6 +2818,7 @@ def _plot_topomap_multi_cbar( names=names, mask=mask, mask_params=mask_params, + mask_label_params=mask_label_params, contours=contours, outlines=outlines, sphere=sphere, @@ -2820,6 +2864,7 @@ def plot_epochs_psd_topomap( names=None, mask=None, mask_params=None, + mask_label_params=None, contours=0, outlines="head", sphere=None, @@ -2866,6 +2911,9 @@ def plot_epochs_psd_topomap( %(names_topomap)s %(mask_evoked_topomap)s %(mask_params_topomap)s + %(mask_label_params_topomap)s + + .. versionadded:: 1.13 %(contours_topomap)s %(outlines_topomap)s %(sphere_topomap_auto)s @@ -2931,6 +2979,7 @@ def plot_psds_topomap( names=None, mask=None, mask_params=None, + mask_label_params=None, contours=0, outlines="head", sphere=None, @@ -2966,6 +3015,9 @@ def plot_psds_topomap( %(names_topomap)s %(mask_evoked_topomap)s %(mask_params_topomap)s + %(mask_label_params_topomap)s + + .. versionadded:: 1.13 %(contours_topomap)s %(outlines_topomap)s %(sphere_topomap_auto)s @@ -3095,6 +3147,7 @@ def plot_psds_topomap( names=names, mask=mask, mask_params=mask_params, + mask_label_params=mask_label_params, contours=contours, image_interp=image_interp, extrapolate=extrapolate, @@ -3303,6 +3356,7 @@ def _topomap_animation( show_names, mask, mask_params, + mask_label_params, contours, outlines, sphere, @@ -3387,6 +3441,7 @@ def _topomap_animation( show_names=show_names, mask=mask, mask_params=mask_params, + mask_label_params=mask_label_params, contours=contours, outlines=outlines, sphere=sphere, @@ -3635,6 +3690,7 @@ def plot_arrowmap( show_names=False, mask=None, mask_params=None, + mask_label_params=None, outlines="head", contours=6, image_interp=_INTERPOLATION_DEFAULT, @@ -3681,6 +3737,9 @@ def plot_arrowmap( If ``True``, a list of names must be provided (see ``names`` keyword). %(mask_topomap)s %(mask_params_topomap)s + %(mask_label_params_topomap)s + + .. versionadded:: 1.13 %(outlines_topomap)s %(contours_topomap)s %(image_interp_topomap)s @@ -3775,6 +3834,7 @@ def plot_arrowmap( res=res, mask=mask, mask_params=mask_params, + mask_label_params=mask_label_params, outlines=outlines, contours=contours, image_interp=image_interp, @@ -4102,6 +4162,7 @@ def plot_regression_weights( show_names=False, mask=None, mask_params=None, + mask_label_params=None, contours=6, outlines="head", sphere=None, @@ -4130,6 +4191,9 @@ def plot_regression_weights( %(show_names_topomap)s %(mask_topomap)s %(mask_params_topomap)s + %(mask_label_params_topomap)s + + .. versionadded:: 1.13 %(contours_topomap)s %(outlines_topomap)s %(sphere_topomap_auto)s @@ -4244,6 +4308,7 @@ def plot_regression_weights( names=names, mask=mask, mask_params=mask_params, + mask_label_params=mask_label_params, contours=contours, image_interp=image_interp, extrapolate=extrapolate,