From 2c307fe777481c5dab77332e027b20a0902c1ba1 Mon Sep 17 00:00:00 2001 From: Marijn van Vliet Date: Wed, 4 Oct 2023 20:15:08 +0300 Subject: [PATCH 01/14] First working version of lasso select in plot_evoked_topo --- mne/viz/topo.py | 32 +++++++--- mne/viz/ui_events.py | 20 +++++++ mne/viz/utils.py | 139 +++++++++++++++++++++++++------------------ 3 files changed, 125 insertions(+), 66 deletions(-) diff --git a/mne/viz/topo.py b/mne/viz/topo.py index a01ee72a0c2..ceab61a0e0c 100644 --- a/mne/viz/topo.py +++ b/mne/viz/topo.py @@ -26,6 +26,7 @@ _setup_ax_spines, _check_cov, _plot_masked_image, + SelectFromCollection, ) @@ -195,8 +196,11 @@ def format_coord_multiaxis(x, y, ch_name=None): under_ax.set(xlim=[0, 1], ylim=[0, 1]) axs = list() + + shown_ch_names = [] for idx, name in iter_ch: ch_idx = ch_names.index(name) + shown_ch_names.append(name) if not unified: # old, slow way ax = plt.axes(pos[idx]) ax.patch.set_facecolor(axis_facecolor) @@ -237,15 +241,22 @@ def format_coord_multiaxis(x, y, ch_name=None): ], [1, 0, 2], ) - if not img: - under_ax.add_collection( - collections.PolyCollection( - verts, - facecolor=axis_facecolor, - edgecolor=axis_spinecolor, - linewidth=1.0, - ) - ) # Not needed for image plots. + if not img: # Not needed for image plots. + collection = collections.PolyCollection( + verts, + facecolor=axis_facecolor, + edgecolor=axis_spinecolor, + ) + under_ax.add_collection(collection) + fig.lasso = SelectFromCollection( + ax=under_ax, + collection=collection, + names=shown_ch_names, + alpha_nonselected=0, + alpha_selected=1, + linewidth_nonselected=0, + linewidth_selected=0.7, + ) for ax in axs: yield ax, ax._mne_ch_idx @@ -344,6 +355,9 @@ def _plot_topo_onpick(event, show_func): """Onpick callback that shows a single channel in a new figure.""" # make sure that the swipe gesture in OS-X doesn't open many figures orig_ax = event.inaxes + if orig_ax.figure.canvas._key in ["shift", "alt"]: + return + import matplotlib.pyplot as plt try: diff --git a/mne/viz/ui_events.py b/mne/viz/ui_events.py index ba5b1db9a33..c4a90e44132 100644 --- a/mne/viz/ui_events.py +++ b/mne/viz/ui_events.py @@ -192,6 +192,26 @@ class Contours(UIEvent): contours: List[str] +@dataclass +@fill_doc +class ChannelsSelect(UIEvent): + """Indicates that the user has selected one or more channels. + + Parameters + ---------- + ch_names : list of str + The names of the channels that were selected. + + Attributes + ---------- + %(ui_event_name_source)s + ch_names : list of str + The names of the channels that were selected. + """ + + ch_names: List[str] + + def _get_event_channel(fig): """Get the event channel associated with a figure. diff --git a/mne/viz/utils.py b/mne/viz/utils.py index 264505b67ad..75ede905d96 100644 --- a/mne/viz/utils.py +++ b/mne/viz/utils.py @@ -64,6 +64,7 @@ _check_decim, ) from ..transforms import apply_trans +from .ui_events import publish, subscribe, ChannelsSelect _channel_type_prettyprint = { @@ -1044,7 +1045,7 @@ def plot_sensors( Whether to plot the sensors as 3d, topomap or as an interactive sensor selection dialog. Available options ``'topomap'``, ``'3d'``, ``'select'``. If ``'select'``, a set of channels can be selected - interactively by using lasso selector or clicking while holding control + interactively by using lasso selector or clicking while holding the shift key. The selected channels are returned along with the figure instance. Defaults to ``'topomap'``. ch_type : None | str @@ -1255,7 +1256,7 @@ def _onpick_sensor(event, fig, ax, pos, ch_names, show_names): if event.mouseevent.inaxes != ax: return - if event.mouseevent.key == "control" and fig.lasso is not None: + if event.mouseevent.key in ["shift", "alt"] and fig.lasso is not None: for ind in event.ind: fig.lasso.select_one(ind) @@ -1360,7 +1361,7 @@ def _plot_sensors( lw=linewidth, ) if kind == "select": - fig.lasso = SelectFromCollection(ax, pts, ch_names) + fig.lasso = SelectFromCollection(ax, pts, names=ch_names) else: fig.lasso = None @@ -1693,11 +1694,11 @@ def _draw_without_rendering(cbar): class SelectFromCollection: - """Select channels from a matplotlib collection using ``LassoSelector``. + """Select objects from a matplotlib collection using ``LassoSelector``. - Selected channels are saved in the ``selection`` attribute. This tool - highlights selected points by fading other points out (i.e., reducing their - alpha values). + The names of the selected objects are saved in the ``selection`` attribute. + This tool highlights selected objects by fading other objects out (i.e., + reducing their alpha values). Parameters ---------- @@ -1705,60 +1706,83 @@ class SelectFromCollection: Axes to interact with. collection : instance of matplotlib collection Collection you want to select from. - alpha_other : 0 <= float <= 1 - To highlight a selection, this tool sets all selected points to an - alpha value of 1 and non-selected points to ``alpha_other``. - Defaults to 0.3. - linewidth_other : float - Linewidth to use for non-selected sensors. Default is 1. + names : list of str + The names of the object. The selection is returned as a subset of these names. + alpha_selected : float + Alpha for selected objects (0=tranparant, 1=opaque). + alpha_nonselected : float + Alpha for non-selected objects (0=tranparant, 1=opaque). + linewidth_selected : float + Linewidth for the borders of selected objects. + linewidth_nonselected : float + Linewidth for the borders of non-selected objects. Notes ----- - This tool selects collection objects based on their *origins* - (i.e., ``offsets``). Calls all callbacks in self.callbacks when selection - is ready. + This tool selects collection objects which bounding boxes intersect with a lasso + path. Calls all callbacks in self.callbacks when selection is ready. """ def __init__( self, ax, collection, - ch_names, - alpha_other=0.5, - linewidth_other=0.5, + *, + names, alpha_selected=1, + alpha_nonselected=0.5, linewidth_selected=1, + linewidth_nonselected=0.5, ): from matplotlib.widgets import LassoSelector + self.fig = ax.figure self.canvas = ax.figure.canvas self.collection = collection - self.ch_names = ch_names - self.alpha_other = alpha_other - self.linewidth_other = linewidth_other + self.names = names self.alpha_selected = alpha_selected + self.alpha_nonselected = alpha_nonselected self.linewidth_selected = linewidth_selected + self.linewidth_nonselected = linewidth_nonselected + + from matplotlib.collections import PolyCollection + from matplotlib.path import Path - self.xys = collection.get_offsets() - self.Npts = len(self.xys) + if isinstance(collection, PolyCollection): + self.paths = collection.get_paths() + else: + self.paths = [Path([point]) for point in collection.get_offsets()] + self.Npts = len(self.paths) + if self.Npts != len(names): + raise ValueError( + f"Number of names ({len(names)}) does not match the number of objects " + f"in the collection ({self.Npts})." + ) - # Ensure that we have separate colors for each object + # Ensure that we have colors for each object. self.fc = collection.get_facecolors() self.ec = collection.get_edgecolors() - self.lw = collection.get_linewidths() if len(self.fc) == 0: raise ValueError("Collection must have a facecolor") elif len(self.fc) == 1: self.fc = np.tile(self.fc, self.Npts).reshape(self.Npts, -1) + if len(self.ec) == 0: + self.ec = np.zeros((self.Npts, 4)) # all black + elif len(self.ec) == 1: self.ec = np.tile(self.ec, self.Npts).reshape(self.Npts, -1) - self.fc[:, -1] = self.alpha_other # deselect in the beginning - self.ec[:, -1] = self.alpha_other - self.lw = np.full(self.Npts, self.linewidth_other) + self.lw = np.full(self.Npts, float(self.linewidth_nonselected)) + # Initialize the lasso selector line_kw = _prop_kw("line", dict(color="red", linewidth=0.5)) self.lasso = LassoSelector(ax, onselect=self.on_select, **line_kw) self.selection = list() - self.callbacks = list() + self.selection_inds = np.array([], dtype="int") + + # Deselect everything in the beginning. + self.style_objects([]) + + # Respond to UI-Events + subscribe(self.fig, "channels_select", self._on_channels_select) def on_select(self, verts): """Select a subset from the collection.""" @@ -1768,44 +1792,45 @@ def on_select(self, verts): return path = Path(verts) - inds = np.nonzero([path.contains_point(xy) for xy in self.xys])[0] - if self.canvas._key == "control": # Appending selection. - sels = [np.where(self.ch_names == c)[0][0] for c in self.selection] - inters = set(inds) - set(sels) - inds = list(inters.union(set(sels) - set(inds))) + inds = np.nonzero([path.intersects_path(p) for p in self.paths])[0] + if self.canvas._key == "shift": # Appending selection. + self.selection_inds = np.union1d(self.selection_inds, inds) + elif self.canvas._key == "alt": # Removing selection. + self.selection_inds = np.setdiff1d(self.selection_inds, inds) + else: + self.selection_inds = inds + ch_names = [self.names[i] for i in self.selection_inds] + publish(self.fig, ChannelsSelect(ch_names=ch_names)) - self.selection[:] = np.array(self.ch_names)[inds].tolist() - self.style_sensors(inds) - self.notify() + def _on_channels_select(self, event): + ch_inds = {name: i for i, name in enumerate(self.names)} + self.selection = [name for name in event.ch_names if name in ch_inds] + self.selection_inds = [ch_inds[name] for name in self.selection] + self.style_objects(self.selection_inds) def select_one(self, ind): """Select or deselect one sensor.""" - ch_name = self.ch_names[ind] - if ch_name in self.selection: - sel_ind = self.selection.index(ch_name) - self.selection.pop(sel_ind) + if self.canvas._key == "shift": + self.selection_inds = np.union1d(self.selection_inds, [ind]) + elif self.canvas._key == "alt": + self.selection_inds = np.setdiff1d(self.selection_inds, [ind]) else: - self.selection.append(ch_name) - inds = np.isin(self.ch_names, self.selection).nonzero()[0] - self.style_sensors(inds) - self.notify() - - def notify(self): - """Notify listeners that a selection has been made.""" - for callback in self.callbacks: - callback() + return # don't notify() + ch_names = [self.names[i] for i in self.selection_inds] + publish(self.fig, ChannelsSelect(ch_names=ch_names)) def select_many(self, inds): """Select many sensors using indices (for predefined selections).""" - self.selection[:] = np.array(self.ch_names)[inds].tolist() - self.style_sensors(inds) + self.selected_inds = inds + ch_names = [self.names[i] for i in self.selection_inds] + publish(self.fig, ChannelsSelect(ch_names=ch_names)) - def style_sensors(self, inds): + def style_objects(self, inds): """Style selected sensors as "active".""" # reset - self.fc[:, -1] = self.alpha_other - self.ec[:, -1] = self.alpha_other / 2 - self.lw[:] = self.linewidth_other + self.fc[:, -1] = self.alpha_nonselected + self.ec[:, -1] = self.alpha_nonselected / 2 + self.lw[:] = self.linewidth_nonselected # style sensors at `inds` self.fc[inds, -1] = self.alpha_selected self.ec[inds, -1] = self.alpha_selected From f6738e71b8f73639dcfd5a4d5d3c837c22ed988f Mon Sep 17 00:00:00 2001 From: Marijn van Vliet Date: Tue, 14 Nov 2023 10:32:45 +0200 Subject: [PATCH 02/14] Fix bug --- mne/viz/tests/test_raw.py | 10 +++++----- mne/viz/utils.py | 4 +++- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/mne/viz/tests/test_raw.py b/mne/viz/tests/test_raw.py index a4c73e76075..ce3eb510ffb 100644 --- a/mne/viz/tests/test_raw.py +++ b/mne/viz/tests/test_raw.py @@ -1063,8 +1063,8 @@ def test_plot_sensors(raw): assert fig.lasso.selection == [] _fake_click(fig, ax, (0.65, 1), xform="ax", kind="motion") _fake_click(fig, ax, (0.65, 0.7), xform="ax", kind="motion") - _fake_keypress(fig, "control") - _fake_click(fig, ax, (0, 0.7), xform="ax", kind="release", key="control") + _fake_keypress(fig, "shift") + _fake_click(fig, ax, (0, 0.7), xform="ax", kind="release", key="shift") assert fig.lasso.selection == ["MEG 0121"] # check that point appearance changes @@ -1073,11 +1073,11 @@ def test_plot_sensors(raw): assert (fc[:, -1] == [0.5, 1.0, 0.5]).all() assert (ec[:, -1] == [0.25, 1.0, 0.25]).all() - _fake_click(fig, ax, (0.7, 1), xform="ax", kind="motion", key="control") + _fake_click(fig, ax, (0.7, 1), xform="ax", kind="motion", key="shift") xy = ax.collections[0].get_offsets() - _fake_click(fig, ax, xy[2], xform="data", key="control") # single sel + _fake_click(fig, ax, xy[2], xform="data", key="shift") # single sel assert fig.lasso.selection == ["MEG 0121", "MEG 0131"] - _fake_click(fig, ax, xy[2], xform="data", key="control") # deselect + _fake_click(fig, ax, xy[2], xform="data", key="shift") # deselect assert fig.lasso.selection == ["MEG 0121"] plt.close("all") diff --git a/mne/viz/utils.py b/mne/viz/utils.py index 4d9a6de3c71..159ad3082b3 100644 --- a/mne/viz/utils.py +++ b/mne/viz/utils.py @@ -1746,7 +1746,9 @@ def on_select(self, verts): def _on_channels_select(self, event): ch_inds = {name: i for i, name in enumerate(self.names)} self.selection = [name for name in event.ch_names if name in ch_inds] - self.selection_inds = [ch_inds[name] for name in self.selection] + self.selection_inds = np.array( + [ch_inds[name] for name in self.selection] + ).astype("int") self.style_objects(self.selection_inds) def select_one(self, ind): From a83b8fd4dc77807f85022ba5cb47fbe3f27586c4 Mon Sep 17 00:00:00 2001 From: Marijn van Vliet Date: Tue, 14 Nov 2023 10:51:41 +0200 Subject: [PATCH 03/14] Fix more renames --- mne/viz/_figure.py | 4 ++-- mne/viz/_mpl_figure.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mne/viz/_figure.py b/mne/viz/_figure.py index 7f958657876..574ea4ea515 100644 --- a/mne/viz/_figure.py +++ b/mne/viz/_figure.py @@ -494,11 +494,11 @@ def _create_ch_location_fig(self, pick): show=False, ) # highlight desired channel & disable interactivity - inds = np.isin(fig.lasso.ch_names, [ch_name]) + inds = np.isin(fig.lasso.names, [ch_name]) fig.lasso.disconnect() fig.lasso.alpha_other = 0.3 fig.lasso.linewidth_selected = 3 - fig.lasso.style_sensors(inds) + fig.lasso.style_objects(inds) return fig diff --git a/mne/viz/_mpl_figure.py b/mne/viz/_mpl_figure.py index b0a059c97cf..223a76b76eb 100644 --- a/mne/viz/_mpl_figure.py +++ b/mne/viz/_mpl_figure.py @@ -1553,7 +1553,7 @@ def _update_selection(self): def _update_highlighted_sensors(self): """Update the sensor plot to show what is selected.""" inds = np.isin( - self.mne.fig_selection.lasso.ch_names, self.mne.ch_names[self.mne.picks] + self.mne.fig_selection.lasso.names, self.mne.ch_names[self.mne.picks] ).nonzero()[0] self.mne.fig_selection.lasso.select_many(inds) From 3bfe2a5f92bf461e20f82cb7057990245075095d Mon Sep 17 00:00:00 2001 From: Marijn van Vliet Date: Tue, 14 Nov 2023 11:24:52 +0200 Subject: [PATCH 04/14] Don't draw patches for channels that do not exist --- mne/viz/topo.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/mne/viz/topo.py b/mne/viz/topo.py index a980e4b41f7..7f98d496a92 100644 --- a/mne/viz/topo.py +++ b/mne/viz/topo.py @@ -235,12 +235,13 @@ def format_coord_multiaxis(x, y, ch_name=None): if unified: under_ax._mne_axs = axs # Create a PolyCollection for the axis backgrounds + sel_pos = pos[[i[0] for i in iter_ch]] verts = np.transpose( [ - pos[:, :2], - pos[:, :2] + pos[:, 2:] * [1, 0], - pos[:, :2] + pos[:, 2:], - pos[:, :2] + pos[:, 2:] * [0, 1], + sel_pos[:, :2], + sel_pos[:, :2] + sel_pos[:, 2:] * [1, 0], + sel_pos[:, :2] + sel_pos[:, 2:], + sel_pos[:, :2] + sel_pos[:, 2:] * [0, 1], ], [1, 0, 2], ) From 2e94752d9062b5ec25b0ae2c46e0d696db48a3f2 Mon Sep 17 00:00:00 2001 From: Marijn van Vliet Date: Tue, 14 Nov 2023 12:00:55 +0200 Subject: [PATCH 05/14] Move the ChannelsSelect ui-event one abstraction layer higher --- mne/viz/topo.py | 13 +++++++++++ mne/viz/utils.py | 60 ++++++++++++++++++++++++++++++------------------ 2 files changed, 51 insertions(+), 22 deletions(-) diff --git a/mne/viz/topo.py b/mne/viz/topo.py index 7f98d496a92..353222d2431 100644 --- a/mne/viz/topo.py +++ b/mne/viz/topo.py @@ -16,6 +16,7 @@ from .._fiff.pick import channel_type, pick_types from ..defaults import _handle_default from ..utils import Bunch, _check_option, _clean_names, _to_rgb, fill_doc +from .ui_events import ChannelsSelect, disable_ui_events, publish, subscribe from .utils import ( DraggableColorbar, SelectFromCollection, @@ -261,6 +262,18 @@ def format_coord_multiaxis(x, y, ch_name=None): linewidth_nonselected=0, linewidth_selected=0.7, ) + + def on_select(): + publish(fig, ChannelsSelect(ch_names=fig.lasso.selection)) + + def on_channels_select(event): + ch_inds = {name: i for i, name in enumerate(ch_names)} + selection_inds = [ch_inds[name] for name in event.ch_names] + with disable_ui_events(fig): + fig.lasso.select_many(selection_inds) + + fig.lasso.callbacks.append(on_select) + subscribe(fig, "channels_select", on_channels_select) for ax in axs: yield ax, ax._mne_ch_idx diff --git a/mne/viz/utils.py b/mne/viz/utils.py index 159ad3082b3..52889b283ae 100644 --- a/mne/viz/utils.py +++ b/mne/viz/utils.py @@ -64,7 +64,13 @@ verbose, warn, ) -from .ui_events import ChannelsSelect, ColormapRange, publish, subscribe +from .ui_events import ( + ChannelsSelect, + ColormapRange, + disable_ui_events, + publish, + subscribe, +) _channel_type_prettyprint = { "eeg": "EEG channel", @@ -1304,6 +1310,18 @@ def _plot_sensors_2d( ) if kind == "select": fig.lasso = SelectFromCollection(ax, pts, names=ch_names) + + def on_select(): + publish(fig, ChannelsSelect(ch_names=fig.lasso.selection)) + + def on_channels_select(event): + ch_inds = {name: i for i, name in enumerate(ch_names)} + selection_inds = [ch_inds[name] for name in event.ch_names] + with disable_ui_events(fig): + fig.lasso.select_many(selection_inds) + + fig.lasso.callbacks.append(on_select) + subscribe(fig, "channels_select", on_channels_select) else: fig.lasso = None @@ -1718,12 +1736,15 @@ def __init__( self.lasso = LassoSelector(ax, onselect=self.on_select, **line_kw) self.selection = list() self.selection_inds = np.array([], dtype="int") + self.callbacks = list() # Deselect everything in the beginning. - self.style_objects([]) + self.style_objects() - # Respond to UI-Events - subscribe(self.fig, "channels_select", self._on_channels_select) + def notify(self): + """Notify listeners that a selection has been made.""" + for callback in self.callbacks: + callback() def on_select(self, verts): """Select a subset from the collection.""" @@ -1740,16 +1761,9 @@ def on_select(self, verts): self.selection_inds = np.setdiff1d(self.selection_inds, inds) else: self.selection_inds = inds - ch_names = [self.names[i] for i in self.selection_inds] - publish(self.fig, ChannelsSelect(ch_names=ch_names)) - - def _on_channels_select(self, event): - ch_inds = {name: i for i, name in enumerate(self.names)} - self.selection = [name for name in event.ch_names if name in ch_inds] - self.selection_inds = np.array( - [ch_inds[name] for name in self.selection] - ).astype("int") - self.style_objects(self.selection_inds) + self.selection = [self.names[i] for i in self.selection_inds] + self.style_objects() + self.notify() def select_one(self, ind): """Select or deselect one sensor.""" @@ -1759,25 +1773,27 @@ def select_one(self, ind): self.selection_inds = np.setdiff1d(self.selection_inds, [ind]) else: return # don't notify() - ch_names = [self.names[i] for i in self.selection_inds] - publish(self.fig, ChannelsSelect(ch_names=ch_names)) + self.selection = [self.names[i] for i in self.selection_inds] + self.style_objects() + self.notify() def select_many(self, inds): """Select many sensors using indices (for predefined selections).""" self.selected_inds = inds - ch_names = [self.names[i] for i in self.selection_inds] - publish(self.fig, ChannelsSelect(ch_names=ch_names)) + self.selection = [self.names[i] for i in self.selection_inds] + self.style_objects() + self.notify() - def style_objects(self, inds): + def style_objects(self): """Style selected sensors as "active".""" # reset self.fc[:, -1] = self.alpha_nonselected self.ec[:, -1] = self.alpha_nonselected / 2 self.lw[:] = self.linewidth_nonselected # style sensors at `inds` - self.fc[inds, -1] = self.alpha_selected - self.ec[inds, -1] = self.alpha_selected - self.lw[inds] = self.linewidth_selected + self.fc[self.selection_inds, -1] = self.alpha_selected + self.ec[self.selection_inds, -1] = self.alpha_selected + self.lw[self.selection_inds] = self.linewidth_selected self.collection.set_facecolors(self.fc) self.collection.set_edgecolors(self.ec) self.collection.set_linewidths(self.lw) From 3c6b73cf9f353648e5572154a10c4fce2a52f34b Mon Sep 17 00:00:00 2001 From: Marijn van Vliet Date: Tue, 14 Nov 2023 13:45:51 +0200 Subject: [PATCH 06/14] Some more fixes --- mne/viz/_figure.py | 4 ++-- mne/viz/utils.py | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/mne/viz/_figure.py b/mne/viz/_figure.py index 574ea4ea515..7cc0d059826 100644 --- a/mne/viz/_figure.py +++ b/mne/viz/_figure.py @@ -494,11 +494,11 @@ def _create_ch_location_fig(self, pick): show=False, ) # highlight desired channel & disable interactivity - inds = np.isin(fig.lasso.names, [ch_name]) + fig.lasst.selection_inds = np.isin(fig.lasso.names, [ch_name]) fig.lasso.disconnect() fig.lasso.alpha_other = 0.3 fig.lasso.linewidth_selected = 3 - fig.lasso.style_objects(inds) + fig.lasso.style_objects() return fig diff --git a/mne/viz/utils.py b/mne/viz/utils.py index 52889b283ae..f69dceffb90 100644 --- a/mne/viz/utils.py +++ b/mne/viz/utils.py @@ -1741,6 +1741,11 @@ def __init__( # Deselect everything in the beginning. self.style_objects() + # For backwards compatibility + @property + def ch_names(self): + return self.names + def notify(self): """Notify listeners that a selection has been made.""" for callback in self.callbacks: From 9b6bd6007d235e1e2b1405cfcdaafb311bb73931 Mon Sep 17 00:00:00 2001 From: Marijn van Vliet Date: Tue, 14 Nov 2023 13:48:48 +0200 Subject: [PATCH 07/14] select_many should not notify() --- mne/viz/topo.py | 5 ++--- mne/viz/utils.py | 12 ++---------- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/mne/viz/topo.py b/mne/viz/topo.py index 353222d2431..cdb5750e84c 100644 --- a/mne/viz/topo.py +++ b/mne/viz/topo.py @@ -16,7 +16,7 @@ from .._fiff.pick import channel_type, pick_types from ..defaults import _handle_default from ..utils import Bunch, _check_option, _clean_names, _to_rgb, fill_doc -from .ui_events import ChannelsSelect, disable_ui_events, publish, subscribe +from .ui_events import ChannelsSelect, publish, subscribe from .utils import ( DraggableColorbar, SelectFromCollection, @@ -269,8 +269,7 @@ def on_select(): def on_channels_select(event): ch_inds = {name: i for i, name in enumerate(ch_names)} selection_inds = [ch_inds[name] for name in event.ch_names] - with disable_ui_events(fig): - fig.lasso.select_many(selection_inds) + fig.lasso.select_many(selection_inds) fig.lasso.callbacks.append(on_select) subscribe(fig, "channels_select", on_channels_select) diff --git a/mne/viz/utils.py b/mne/viz/utils.py index f69dceffb90..5d5dae51c26 100644 --- a/mne/viz/utils.py +++ b/mne/viz/utils.py @@ -64,13 +64,7 @@ verbose, warn, ) -from .ui_events import ( - ChannelsSelect, - ColormapRange, - disable_ui_events, - publish, - subscribe, -) +from .ui_events import ChannelsSelect, ColormapRange, publish, subscribe _channel_type_prettyprint = { "eeg": "EEG channel", @@ -1317,8 +1311,7 @@ def on_select(): def on_channels_select(event): ch_inds = {name: i for i, name in enumerate(ch_names)} selection_inds = [ch_inds[name] for name in event.ch_names] - with disable_ui_events(fig): - fig.lasso.select_many(selection_inds) + fig.lasso.select_many(selection_inds) fig.lasso.callbacks.append(on_select) subscribe(fig, "channels_select", on_channels_select) @@ -1787,7 +1780,6 @@ def select_many(self, inds): self.selected_inds = inds self.selection = [self.names[i] for i in self.selection_inds] self.style_objects() - self.notify() def style_objects(self): """Style selected sensors as "active".""" From 573cb40151d4934d95bbdd5f757049f0164d67d7 Mon Sep 17 00:00:00 2001 From: Marijn van Vliet Date: Tue, 14 Nov 2023 14:23:27 +0200 Subject: [PATCH 08/14] Add "select" parameter to enable/disable the lasso selection tool --- mne/viz/_figure.py | 2 +- mne/viz/evoked.py | 8 +++++- mne/viz/topo.py | 66 +++++++++++++++++++++++++++++++++------------- 3 files changed, 55 insertions(+), 21 deletions(-) diff --git a/mne/viz/_figure.py b/mne/viz/_figure.py index 7cc0d059826..d292732c276 100644 --- a/mne/viz/_figure.py +++ b/mne/viz/_figure.py @@ -494,7 +494,7 @@ def _create_ch_location_fig(self, pick): show=False, ) # highlight desired channel & disable interactivity - fig.lasst.selection_inds = np.isin(fig.lasso.names, [ch_name]) + fig.lasso.selection_inds = np.isin(fig.lasso.names, [ch_name]) fig.lasso.disconnect() fig.lasso.alpha_other = 0.3 fig.lasso.linewidth_selected = 3 diff --git a/mne/viz/evoked.py b/mne/viz/evoked.py index 1c6712a6bec..9ebee6e69f4 100644 --- a/mne/viz/evoked.py +++ b/mne/viz/evoked.py @@ -1178,6 +1178,7 @@ def plot_evoked_topo( background_color="w", noise_cov=None, exclude="bads", + select=False, show=True, ): """Plot 2D topography of evoked responses. @@ -1248,6 +1249,10 @@ def plot_evoked_topo( exclude : list of str | 'bads' Channels names to exclude from the plot. If 'bads', the bad channels are excluded. By default, exclude is set to 'bads'. + select : bool + Whether to enable the lasso-selection tool to enable the user to select + channels. The selected channels will be available in + ``fig.lasso.selection``. show : bool Show figure if True. @@ -1304,10 +1309,11 @@ def plot_evoked_topo( font_color=font_color, merge_channels=merge_grads, legend=legend, + noise_cov=noise_cov, axes=axes, exclude=exclude, + select=select, show=show, - noise_cov=noise_cov, ) diff --git a/mne/viz/topo.py b/mne/viz/topo.py index cdb5750e84c..fe815c3ce45 100644 --- a/mne/viz/topo.py +++ b/mne/viz/topo.py @@ -42,6 +42,7 @@ def iter_topography( axis_spinecolor="k", layout_scale=None, legend=False, + select=False, ): """Create iterator over channel positions. @@ -77,6 +78,10 @@ def iter_topography( If True, an additional axis is created in the bottom right corner that can be used to, e.g., construct a legend. The index of this axis will be -1. + select : bool + Whether to enable the lasso-selection tool to enable the user to select + channels. The selected channels will be available in + ``fig.lasso.selection``. Returns ------- @@ -98,6 +103,7 @@ def iter_topography( axis_spinecolor, layout_scale, legend=legend, + select=select, ) @@ -133,6 +139,7 @@ def _iter_topography( img=False, axes=None, legend=False, + select=False, ): """Iterate over topography. @@ -251,28 +258,32 @@ def format_coord_multiaxis(x, y, ch_name=None): verts, facecolor=axis_facecolor, edgecolor=axis_spinecolor, + linewidth=1.0, ) under_ax.add_collection(collection) - fig.lasso = SelectFromCollection( - ax=under_ax, - collection=collection, - names=shown_ch_names, - alpha_nonselected=0, - alpha_selected=1, - linewidth_nonselected=0, - linewidth_selected=0.7, - ) - def on_select(): - publish(fig, ChannelsSelect(ch_names=fig.lasso.selection)) + if select: + # Configure the lasso-selection tool + fig.lasso = SelectFromCollection( + ax=under_ax, + collection=collection, + names=shown_ch_names, + alpha_nonselected=0, + alpha_selected=1, + linewidth_nonselected=0, + linewidth_selected=0.7, + ) + + def on_select(): + publish(fig, ChannelsSelect(ch_names=fig.lasso.selection)) - def on_channels_select(event): - ch_inds = {name: i for i, name in enumerate(ch_names)} - selection_inds = [ch_inds[name] for name in event.ch_names] - fig.lasso.select_many(selection_inds) + def on_channels_select(event): + ch_inds = {name: i for i, name in enumerate(ch_names)} + selection_inds = [ch_inds[name] for name in event.ch_names] + fig.lasso.select_many(selection_inds) - fig.lasso.callbacks.append(on_select) - subscribe(fig, "channels_select", on_channels_select) + fig.lasso.callbacks.append(on_select) + subscribe(fig, "channels_select", on_channels_select) for ax in axs: yield ax, ax._mne_ch_idx @@ -299,6 +310,7 @@ def _plot_topo( unified=False, img=False, axes=None, + select=False, ): """Plot on sensor layout.""" import matplotlib.pyplot as plt @@ -351,6 +363,7 @@ def _plot_topo( unified=unified, img=img, axes=axes, + select=select, ) for ax, ch_idx in my_topo_plot: @@ -873,9 +886,10 @@ def _plot_evoked_topo( merge_channels=False, legend=True, axes=None, + noise_cov=None, exclude="bads", + select=False, show=True, - noise_cov=None, ): """Plot 2D topography of evoked responses. @@ -947,6 +961,10 @@ def _plot_evoked_topo( exclude : list of str | 'bads' Channels names to exclude from being shown. If 'bads', the bad channels are excluded. By default, exclude is set to 'bads'. + select : bool + Whether to enable the lasso-selection tool to enable the user to select + channels. The selected channels will be available in + ``fig.lasso.selection``. show : bool Show figure if True. @@ -1124,6 +1142,7 @@ def _plot_evoked_topo( y_label=y_label, unified=True, axes=axes, + select=select, ) add_background_image(fig, fig_background) @@ -1131,7 +1150,10 @@ def _plot_evoked_topo( if legend is not False: legend_loc = 0 if legend is True else legend labels = [e.comment if e.comment else "Unknown" for e in evoked] - handles = fig.axes[0].lines[: len(evoked)] + if select: + handles = fig.axes[0].lines[1 : len(evoked) + 1] + else: + handles = fig.axes[0].lines[: len(evoked)] legend = plt.legend( labels=labels, handles=handles, loc=legend_loc, prop={"size": 10} ) @@ -1190,6 +1212,7 @@ def plot_topo_image_epochs( fig_facecolor="k", fig_background=None, font_color="w", + select=False, show=True, ): """Plot Event Related Potential / Fields image on topographies. @@ -1237,6 +1260,10 @@ def plot_topo_image_epochs( :func:`matplotlib.pyplot.imshow`. Defaults to ``None``. font_color : color The color of tick labels in the colorbar. Defaults to white. + select : bool + Whether to enable the lasso-selection tool to enable the user to select + channels. The selected channels will be available in + ``fig.lasso.selection``. show : bool Whether to show the figure. Defaults to ``True``. @@ -1326,6 +1353,7 @@ def plot_topo_image_epochs( y_label="Epoch", unified=True, img=True, + select=select, ) add_background_image(fig, fig_background) plt_show(show) From facd394b5f05ff5effeced8d7466be520e0a4826 Mon Sep 17 00:00:00 2001 From: Marijn van Vliet Date: Tue, 14 Nov 2023 14:51:02 +0200 Subject: [PATCH 09/14] Add select parameter to relevant methods --- mne/epochs.py | 2 ++ mne/evoked.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/mne/epochs.py b/mne/epochs.py index b7afada3d1a..e3d625b3401 100644 --- a/mne/epochs.py +++ b/mne/epochs.py @@ -1345,6 +1345,7 @@ def plot_topo_image( fig_facecolor="k", fig_background=None, font_color="w", + select=False, show=True, ): return plot_topo_image_epochs( @@ -1363,6 +1364,7 @@ def plot_topo_image( fig_facecolor=fig_facecolor, fig_background=fig_background, font_color=font_color, + select=select, show=show, ) diff --git a/mne/evoked.py b/mne/evoked.py index f2c75f3754b..a0150a0c2dc 100644 --- a/mne/evoked.py +++ b/mne/evoked.py @@ -555,6 +555,7 @@ def plot_topo( background_color="w", noise_cov=None, exclude="bads", + select=False, show=True, ): """ @@ -580,6 +581,7 @@ def plot_topo( background_color=background_color, noise_cov=noise_cov, exclude=exclude, + select=select, show=show, ) From a0069d82f9fb052b4b4a2d33de4a5ab380a477e5 Mon Sep 17 00:00:00 2001 From: Marijn van Vliet Date: Wed, 15 Nov 2023 10:37:25 +0200 Subject: [PATCH 10/14] Update test --- mne/viz/tests/test_raw.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/mne/viz/tests/test_raw.py b/mne/viz/tests/test_raw.py index ce3eb510ffb..f930d7cbf9d 100644 --- a/mne/viz/tests/test_raw.py +++ b/mne/viz/tests/test_raw.py @@ -1061,10 +1061,10 @@ def test_plot_sensors(raw): _fake_click(fig, ax, (0, 1), xform="ax") fig.canvas.draw() assert fig.lasso.selection == [] - _fake_click(fig, ax, (0.65, 1), xform="ax", kind="motion") - _fake_click(fig, ax, (0.65, 0.7), xform="ax", kind="motion") + _fake_click(fig, ax, (-0.11, 0.14), xform="data", kind="motion") + _fake_click(fig, ax, (-0.11, 0.065), xform="data", kind="motion") _fake_keypress(fig, "shift") - _fake_click(fig, ax, (0, 0.7), xform="ax", kind="release", key="shift") + _fake_click(fig, ax, (-0.15, 0.065), xform="data", kind="release", key="shift") assert fig.lasso.selection == ["MEG 0121"] # check that point appearance changes @@ -1073,11 +1073,11 @@ def test_plot_sensors(raw): assert (fc[:, -1] == [0.5, 1.0, 0.5]).all() assert (ec[:, -1] == [0.25, 1.0, 0.25]).all() - _fake_click(fig, ax, (0.7, 1), xform="ax", kind="motion", key="shift") + _fake_click(fig, ax, (-0.11, 0.065), xform="data", kind="motion", key="shift") xy = ax.collections[0].get_offsets() _fake_click(fig, ax, xy[2], xform="data", key="shift") # single sel assert fig.lasso.selection == ["MEG 0121", "MEG 0131"] - _fake_click(fig, ax, xy[2], xform="data", key="shift") # deselect + _fake_click(fig, ax, xy[2], xform="data", key="alt") # deselect assert fig.lasso.selection == ["MEG 0121"] plt.close("all") From d913fffe705f41ed71d2e618b5f2912958163683 Mon Sep 17 00:00:00 2001 From: Marijn van Vliet Date: Wed, 24 Jul 2024 22:03:38 +0300 Subject: [PATCH 11/14] fix bugs (thanks vulture!) --- mne/viz/_figure.py | 2 +- mne/viz/utils.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mne/viz/_figure.py b/mne/viz/_figure.py index 6b93347b606..2b891e0ebc3 100644 --- a/mne/viz/_figure.py +++ b/mne/viz/_figure.py @@ -503,7 +503,7 @@ def _create_ch_location_fig(self, pick): # highlight desired channel & disable interactivity fig.lasso.selection_inds = np.isin(fig.lasso.names, [ch_name]) fig.lasso.disconnect() - fig.lasso.alpha_other = 0.3 + fig.lasso.alpha_nonselected = 0.3 fig.lasso.linewidth_selected = 3 fig.lasso.style_objects() diff --git a/mne/viz/utils.py b/mne/viz/utils.py index 8af8e60e3ca..d326c5d6875 100644 --- a/mne/viz/utils.py +++ b/mne/viz/utils.py @@ -1744,7 +1744,7 @@ def select_one(self, ind): def select_many(self, inds): """Select many sensors using indices (for predefined selections).""" - self.selected_inds = inds + self.selection_inds = inds self.selection = [self.names[i] for i in self.selection_inds] self.style_objects() From bcef66e2cb444a03b775ae7b1c5d0c91c0a40cff Mon Sep 17 00:00:00 2001 From: Marijn van Vliet Date: Tue, 8 Oct 2024 12:45:56 +0300 Subject: [PATCH 12/14] attempt to fix tests --- mne/viz/tests/test_raw.py | 63 +++++++++++++++++++++++++-------------- mne/viz/utils.py | 10 +++---- 2 files changed, 46 insertions(+), 27 deletions(-) diff --git a/mne/viz/tests/test_raw.py b/mne/viz/tests/test_raw.py index dffeb80e98f..163e55a4dfe 100644 --- a/mne/viz/tests/test_raw.py +++ b/mne/viz/tests/test_raw.py @@ -1090,63 +1090,82 @@ def test_plot_sensors(raw): pytest.raises(TypeError, plot_sensors, raw) # needs to be info pytest.raises(ValueError, plot_sensors, raw.info, kind="sasaasd") plt.close("all") + fig, sels = raw.plot_sensors("select", show_names=True) ax = fig.axes[0] - # Click with no sensors - _fake_click(fig, ax, (0.0, 0.0), xform="data") - _fake_click(fig, ax, (0, 0.0), xform="data", kind="release") + # Lasso with no sensors. + _fake_click(fig, ax, (-0.14, 0.14), xform="data") + _fake_click(fig, ax, (-0.13, 0.13), xform="data", kind="motion") + _fake_click(fig, ax, (-0.13, 0.14), xform="data", kind="motion") + _fake_click(fig, ax, (-0.13, 0.14), xform="data", kind="release") assert fig.lasso.selection == [] - # Lasso with 1 sensor (upper left) - _fake_click(fig, ax, (0, 1), xform="ax") - fig.canvas.draw() + # Lasso with 1 sensor (upper left). + _fake_click(fig, ax, (-0.13, 0.14), xform="data") assert fig.lasso.selection == [] _fake_click(fig, ax, (-0.11, 0.14), xform="data", kind="motion") _fake_click(fig, ax, (-0.11, 0.065), xform="data", kind="motion") - _fake_keypress(fig, "shift") - _fake_click(fig, ax, (-0.15, 0.065), xform="data", kind="release", key="shift") + _fake_click(fig, ax, (-0.13, 0.065), xform="data", kind="motion") + _fake_click(fig, ax, (-0.13, 0.14), xform="ax", kind="motion") + _fake_click(fig, ax, (-0.13, 0.14), xform="ax", kind="release") assert fig.lasso.selection == ["MEG 0121"] - # check that point appearance changes + # Use SHIFT key to lasso an additional sensor. + _fake_keypress(fig, "shift") + _fake_click(fig, ax, (-0.17, 0.07), xform="data") + _fake_click(fig, ax, (-0.17, 0.05), xform="data", kind="motion") + _fake_click(fig, ax, (-0.15, 0.05), xform="data", kind="motion") + _fake_click(fig, ax, (-0.15, 0.07), xform="data", kind="motion") + _fake_click(fig, ax, (-0.15, 0.07), xform="data", kind="release") + _fake_keypress(fig, "shift", kind="release") + assert fig.lasso.selection == ["MEG 0111", "MEG 0121"] + + # Check that the two selected sensors have a different appearance. fc = fig.lasso.collection.get_facecolors() ec = fig.lasso.collection.get_edgecolors() - assert (fc[:, -1] == [0.5, 1.0, 0.5]).all() - assert (ec[:, -1] == [0.25, 1.0, 0.25]).all() - - _fake_click(fig, ax, (-0.11, 0.065), xform="data", kind="motion", key="shift") - xy = ax.collections[0].get_offsets() - _fake_click(fig, ax, xy[2], xform="data", key="shift") # single sel - assert fig.lasso.selection == ["MEG 0121", "MEG 0131"] - _fake_click(fig, ax, xy[2], xform="data", key="alt") # deselect + assert (fc[2:, -1] == 0.5).all() + assert (ec[2:, -1] == 0.25).all() + assert (fc[:2, -1] == 1.0).all() + assert (ec[:2:, -1] == 1.0).all() + + # Use ALT key to remove a sensor from the lasso. + _fake_keypress(fig, "alt") + _fake_click(fig, ax, (-0.17, 0.07), xform="data") + _fake_click(fig, ax, (-0.17, 0.05), xform="data", kind="motion") + _fake_click(fig, ax, (-0.15, 0.05), xform="data", kind="motion") + _fake_click(fig, ax, (-0.15, 0.07), xform="data", kind="motion") + _fake_click(fig, ax, (-0.15, 0.07), xform="data", kind="release") + _fake_keypress(fig, "alt", kind="release") assert fig.lasso.selection == ["MEG 0121"] + plt.close("all") raw.info["dev_head_t"] = None # like empty room with pytest.warns(RuntimeWarning, match="identity"): raw.plot_sensors() - # Test plotting with sphere='eeglab' + # Test plotting with sphere='eeglab'. info = create_info(ch_names=["Fpz", "Oz", "T7", "T8"], sfreq=100, ch_types="eeg") data = 1e-6 * np.random.rand(4, 100) raw_eeg = RawArray(data=data, info=info) raw_eeg.set_montage("biosemi64") raw_eeg.plot_sensors(sphere="eeglab") - # Should work with "FPz" as well + # Should work with "FPz" as well. raw_eeg.rename_channels({"Fpz": "FPz"}) raw_eeg.plot_sensors(sphere="eeglab") - # Should still work without Fpz/FPz, as long as we still have Oz + # Should still work without Fpz/FPz, as long as we still have Oz. raw_eeg.drop_channels("FPz") raw_eeg.plot_sensors(sphere="eeglab") - # Should raise if Oz is missing too, as we cannot reconstruct Fpz anymore + # Should raise if Oz is missing too, as we cannot reconstruct Fpz anymore. raw_eeg.drop_channels("Oz") with pytest.raises(ValueError, match="could not find: Fpz"): raw_eeg.plot_sensors(sphere="eeglab") - # Should raise if we don't have a montage + # Should raise if we don't have a montage. chs = deepcopy(raw_eeg.info["chs"]) raw_eeg.set_montage(None) with raw_eeg.info._unlock(): diff --git a/mne/viz/utils.py b/mne/viz/utils.py index 00d37461f62..e3f26224fd5 100644 --- a/mne/viz/utils.py +++ b/mne/viz/utils.py @@ -807,12 +807,12 @@ def _fake_click(fig, ax, point, xform="ax", button=1, kind="press", key=None): ) -def _fake_keypress(fig, key): +def _fake_keypress(fig, key, kind="press"): from matplotlib import backend_bases fig.canvas.callbacks.process( - "key_press_event", - backend_bases.KeyEvent(name="key_press_event", canvas=fig.canvas, key=key), + f"key_{kind}_event", + backend_bases.KeyEvent(name=f"key_{kind}_event", canvas=fig.canvas, key=key), ) @@ -1715,9 +1715,9 @@ def on_select(self, verts): path = Path(verts) inds = np.nonzero([path.intersects_path(p) for p in self.paths])[0] if self.canvas._key == "shift": # Appending selection. - self.selection_inds = np.union1d(self.selection_inds, inds) + self.selection_inds = np.union1d(self.selection_inds, inds).astype("int") elif self.canvas._key == "alt": # Removing selection. - self.selection_inds = np.setdiff1d(self.selection_inds, inds) + self.selection_inds = np.setdiff1d(self.selection_inds, inds).astype("int") else: self.selection_inds = inds self.selection = [self.names[i] for i in self.selection_inds] From 8efcb8cce1ad2af9b2431929d2a8131d34102ee0 Mon Sep 17 00:00:00 2001 From: Marijn van Vliet Date: Tue, 22 Oct 2024 12:48:21 +0300 Subject: [PATCH 13/14] further attempts to fix tests --- mne/viz/tests/test_raw.py | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/mne/viz/tests/test_raw.py b/mne/viz/tests/test_raw.py index 163e55a4dfe..8a3282b55b7 100644 --- a/mne/viz/tests/test_raw.py +++ b/mne/viz/tests/test_raw.py @@ -1090,25 +1090,23 @@ def test_plot_sensors(raw): pytest.raises(TypeError, plot_sensors, raw) # needs to be info pytest.raises(ValueError, plot_sensors, raw.info, kind="sasaasd") plt.close("all") - fig, sels = raw.plot_sensors("select", show_names=True) ax = fig.axes[0] - # Lasso with no sensors. + # Click with no sensors _fake_click(fig, ax, (-0.14, 0.14), xform="data") _fake_click(fig, ax, (-0.13, 0.13), xform="data", kind="motion") _fake_click(fig, ax, (-0.13, 0.14), xform="data", kind="motion") _fake_click(fig, ax, (-0.13, 0.14), xform="data", kind="release") assert fig.lasso.selection == [] - # Lasso with 1 sensor (upper left). - _fake_click(fig, ax, (-0.13, 0.14), xform="data") - assert fig.lasso.selection == [] - _fake_click(fig, ax, (-0.11, 0.14), xform="data", kind="motion") - _fake_click(fig, ax, (-0.11, 0.065), xform="data", kind="motion") - _fake_click(fig, ax, (-0.13, 0.065), xform="data", kind="motion") - _fake_click(fig, ax, (-0.13, 0.14), xform="ax", kind="motion") - _fake_click(fig, ax, (-0.13, 0.14), xform="ax", kind="release") + # Lasso with 1 sensor (upper left) + _fake_click(fig, ax, (-0.13, 0.13), xform="data") + _fake_click(fig, ax, (-0.11, 0.13), xform="data", kind="motion") + _fake_click(fig, ax, (-0.11, 0.06), xform="data", kind="motion") + _fake_click(fig, ax, (-0.13, 0.06), xform="data", kind="motion") + _fake_click(fig, ax, (-0.13, 0.13), xform="data", kind="motion") + _fake_click(fig, ax, (-0.13, 0.13), xform="data", kind="release") assert fig.lasso.selection == ["MEG 0121"] # Use SHIFT key to lasso an additional sensor. @@ -1137,7 +1135,6 @@ def test_plot_sensors(raw): _fake_click(fig, ax, (-0.15, 0.07), xform="data", kind="motion") _fake_click(fig, ax, (-0.15, 0.07), xform="data", kind="release") _fake_keypress(fig, "alt", kind="release") - assert fig.lasso.selection == ["MEG 0121"] plt.close("all") @@ -1145,27 +1142,27 @@ def test_plot_sensors(raw): with pytest.warns(RuntimeWarning, match="identity"): raw.plot_sensors() - # Test plotting with sphere='eeglab'. + # Test plotting with sphere='eeglab' info = create_info(ch_names=["Fpz", "Oz", "T7", "T8"], sfreq=100, ch_types="eeg") data = 1e-6 * np.random.rand(4, 100) raw_eeg = RawArray(data=data, info=info) raw_eeg.set_montage("biosemi64") raw_eeg.plot_sensors(sphere="eeglab") - # Should work with "FPz" as well. + # Should work with "FPz" as well raw_eeg.rename_channels({"Fpz": "FPz"}) raw_eeg.plot_sensors(sphere="eeglab") - # Should still work without Fpz/FPz, as long as we still have Oz. + # Should still work without Fpz/FPz, as long as we still have Oz raw_eeg.drop_channels("FPz") raw_eeg.plot_sensors(sphere="eeglab") - # Should raise if Oz is missing too, as we cannot reconstruct Fpz anymore. + # Should raise if Oz is missing too, as we cannot reconstruct Fpz anymore raw_eeg.drop_channels("Oz") with pytest.raises(ValueError, match="could not find: Fpz"): raw_eeg.plot_sensors(sphere="eeglab") - # Should raise if we don't have a montage. + # Should raise if we don't have a montage chs = deepcopy(raw_eeg.info["chs"]) raw_eeg.set_montage(None) with raw_eeg.info._unlock(): From 87f72e2ae2ff8f2c18e3725bd930f98abc367d20 Mon Sep 17 00:00:00 2001 From: Marijn van Vliet Date: Tue, 22 Oct 2024 13:07:24 +0300 Subject: [PATCH 14/14] Add what's new entry --- doc/changes/devel/12071.newfeature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/changes/devel/12071.newfeature.rst diff --git a/doc/changes/devel/12071.newfeature.rst b/doc/changes/devel/12071.newfeature.rst new file mode 100644 index 00000000000..4e7995e3beb --- /dev/null +++ b/doc/changes/devel/12071.newfeature.rst @@ -0,0 +1 @@ +Add new ``select`` parameter to :func:`mne.viz.plot_evoked_topo` and :meth:`mne.Evoked.plot_topo` to toggle lasso selection of sensors, by `Marijn van Vliet`_.