Skip to content

Commit

Permalink
Properly implement and test single channel picking
Browse files Browse the repository at this point in the history
  • Loading branch information
wmvanvliet committed Jan 22, 2025
1 parent e806634 commit dbabf05
Show file tree
Hide file tree
Showing 5 changed files with 77 additions and 27 deletions.
11 changes: 10 additions & 1 deletion mne/viz/tests/test_raw.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
set_config,
)
from mne.viz import plot_raw, plot_sensors
from mne.viz.utils import _fake_click
from mne.viz.utils import _fake_click, _fake_keypress

base_dir = Path(__file__).parents[2] / "io" / "tests" / "data"
raw_fname = base_dir / "test_raw.fif"
Expand Down Expand Up @@ -1089,6 +1089,8 @@ def test_plot_sensors(raw):
pytest.raises(ValueError, plot_sensors, raw.info, kind="sasaasd")
plt.close("all")

print(raw.ch_names)

# Test lasso selection.
fig, sels = raw.plot_sensors("select", show_names=True)
ax = fig.axes[0]
Expand All @@ -1100,6 +1102,13 @@ def test_plot_sensors(raw):
_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"]

# Add another sensor with a single click.
_fake_keypress(fig, "control")
_fake_click(fig, ax, (-0.1278, 0.0318), xform="data")
_fake_click(fig, ax, (-0.1278, 0.0318), xform="data", kind="release")
_fake_keypress(fig, "control", kind="release")
assert fig.lasso.selection == ["MEG 0121", "MEG 0131"]
plt.close("all")

raw.info["dev_head_t"] = None # like empty room
Expand Down
21 changes: 19 additions & 2 deletions mne/viz/tests/test_topo.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
)
from mne.viz.evoked import _line_plot_onselect
from mne.viz.topo import _imshow_tfr, _plot_update_evoked_topo_proj, iter_topography
from mne.viz.utils import _fake_click
from mne.viz.utils import _fake_click, _fake_keypress

base_dir = Path(__file__).parents[2] / "io" / "tests" / "data"
evoked_fname = base_dir / "test-ave.fif"
Expand Down Expand Up @@ -310,7 +310,24 @@ def test_plot_topo_select():
"""Test selecting sensors in an ERP topography plot."""
# Show topography
evoked = _get_epochs().average()
plot_evoked_topo(evoked, select=True)
fig = plot_evoked_topo(evoked, select=True)
ax = fig.axes[0]

# Lasso select 3 out of the 6 sensors.
_fake_click(fig, ax, (0.05, 0.5), xform="data")
_fake_click(fig, ax, (0.2, 0.5), xform="data", kind="motion")
_fake_click(fig, ax, (0.2, 0.6), xform="data", kind="motion")
_fake_click(fig, ax, (0.05, 0.6), xform="data", kind="motion")
_fake_click(fig, ax, (0.05, 0.5), xform="data", kind="motion")
_fake_click(fig, ax, (0.05, 0.5), xform="data", kind="release")
assert fig.lasso.selection == ["MEG 0132", "MEG 0133", "MEG 0131"]

# Add another sensor with a single click.
_fake_keypress(fig, "control")
_fake_click(fig, ax, (0.11, 0.65), xform="data")
_fake_click(fig, ax, (0.21, 0.65), xform="data", kind="release")
_fake_keypress(fig, "control", kind="release")
assert fig.lasso.selection == ["MEG 0111", "MEG 0132", "MEG 0133", "MEG 0131"]


def test_plot_tfr_topo():
Expand Down
28 changes: 22 additions & 6 deletions mne/viz/tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,10 @@ def test_select_from_collection():
_fake_click(fig, ax, (0.5, 1), xform="data", kind="release")
assert lasso.selection == []

# Doing a single click on a patch should not select it.
_fake_click(fig, ax, (1, 1), xform="data")
assert lasso.selection == []

# Make a selection with two patches in it.
_fake_click(fig, ax, (0, 0.5), xform="data")
_fake_click(fig, ax, (3, 0.5), xform="data", kind="motion")
Expand All @@ -302,28 +306,40 @@ def test_select_from_collection():
_fake_click(fig, ax, (0, 0.5), xform="data", kind="release")
assert lasso.selection == ["A", "B"]

# Use SHIFT key to lasso an additional patch.
_fake_keypress(fig, "shift")
# Use Control key to lasso an additional patch.
_fake_keypress(fig, "control")
_fake_click(fig, ax, (0.5, -0.5), xform="data")
_fake_click(fig, ax, (1.5, -0.5), xform="data", kind="motion")
_fake_click(fig, ax, (1.5, 0.5), xform="data", kind="motion")
_fake_click(fig, ax, (0.5, 0.5), xform="data", kind="motion")
_fake_click(fig, ax, (0.5, 0.5), xform="data", kind="release")
_fake_keypress(fig, "shift", kind="release")
_fake_keypress(fig, "control", kind="release")
assert lasso.selection == ["A", "B", "D"]

# Use ALT key to remove a patch.
_fake_keypress(fig, "alt")
# Use CTRL+SHIFT to remove a patch.
_fake_keypress(fig, "ctrl+shift")
_fake_click(fig, ax, (0.5, 0.5), xform="data")
_fake_click(fig, ax, (1.5, 0.5), xform="data", kind="motion")
_fake_click(fig, ax, (1.5, 1.5), xform="data", kind="motion")
_fake_click(fig, ax, (0.5, 1.5), xform="data", kind="motion")
_fake_click(fig, ax, (0.5, 1.5), xform="data", kind="release")
_fake_keypress(fig, "alt", kind="release")
_fake_keypress(fig, "ctrl+shift", kind="release")
assert lasso.selection == ["B", "D"]

# Check that the two selected patches have a different appearance.
fc = lasso.collection.get_facecolors()
ec = lasso.collection.get_edgecolors()
assert (fc[:, -1] == [0.5, 1.0, 0.5, 1.0]).all()
assert (ec[:, -1] == [0.25, 1.0, 0.25, 1.0]).all()

# Test adding and removing single channels.
lasso.select_one(2) # should not do anything without modifier keys
assert lasso.selection == ["B", "D"]
_fake_keypress(fig, "control")
lasso.select_one(2) # add to selection
_fake_keypress(fig, "control", kind="release")
assert lasso.selection == ["B", "C", "D"]
_fake_keypress(fig, "ctrl+shift")
lasso.select_one(1) # remove from selection
assert lasso.selection == ["C", "D"]
_fake_keypress(fig, "ctrl+shift", kind="release")
17 changes: 11 additions & 6 deletions mne/viz/topo.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,10 +275,9 @@ 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(shown_ch_names)}
selection_inds = [
ch_inds[name] for name in event.ch_names if name in ch_inds
]
selection_inds = np.flatnonzero(
np.isin(shown_ch_names, event.ch_names)
)
fig.lasso.select_many(selection_inds)

fig.lasso.callbacks.append(on_select)
Expand Down Expand Up @@ -381,9 +380,15 @@ def _plot_topo(

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"]:
fig = orig_ax.figure

# If we are doing lasso select, allow it to handle the click instead.
if fig.lasso is not None and event.key in ["control", "ctrl+shift"]:
return

# make sure that the swipe gesture in OS-X doesn't open many figures
if fig.canvas._key in ["shift", "alt"]:
return

import matplotlib.pyplot as plt
Expand Down
27 changes: 15 additions & 12 deletions mne/viz/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -952,7 +952,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 the shift
interactively by using lasso selector or clicking while holding the control
key. The selected channels are returned along with the figure instance.
Defaults to ``'topomap'``.
ch_type : None | str
Expand Down Expand Up @@ -1163,10 +1163,10 @@ def _onpick_sensor(event, fig, ax, pos, ch_names, show_names):
if event.mouseevent.inaxes != ax:
return

if event.mouseevent.key in ["shift", "alt"] and fig.lasso is not None:
if fig.lasso is not None and event.mouseevent.key in ["control", "ctrl+shift"]:
# Add the sensor to the selection instead of showing its name.
for ind in event.ind:
fig.lasso.select_one(ind)

return
if show_names:
return # channel names already visible
Expand Down Expand Up @@ -1278,10 +1278,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 if name in ch_inds
]
selection_inds = np.flatnonzero(np.isin(ch_names, event.ch_names))
fig.lasso.select_many(selection_inds)

fig.lasso.callbacks.append(on_select)
Expand Down Expand Up @@ -1614,6 +1611,9 @@ class SelectFromCollection:
This tool highlights selected objects by fading other objects out (i.e.,
reducing their alpha values).
Holding down the Control key will add to the current selection, and holding down
Control+Shift will remove from the current selection.
Parameters
----------
ax : instance of Axes
Expand Down Expand Up @@ -1711,14 +1711,17 @@ def on_select(self, verts):
"""Select a subset from the collection."""
from matplotlib.path import Path

if len(verts) <= 3: # Seems to be a good way to exclude single clicks.
# Don't respond to single clicks without extra keys being hold down.
# Figures like plot_evoked_topo want to do something else with them.
print(verts, self.canvas._key)
if len(verts) <= 3 and self.canvas._key not in ["control", "ctrl+shift"]:
return

path = Path(verts)
inds = np.nonzero([path.intersects_path(p) for p in self.paths])[0]
if self.canvas._key == "shift": # Appending selection.
if self.canvas._key == "control": # Appending selection.
self.selection_inds = np.union1d(self.selection_inds, inds).astype("int")
elif self.canvas._key == "alt": # Removing selection.
elif self.canvas._key == "ctrl+shift":
self.selection_inds = np.setdiff1d(self.selection_inds, inds).astype("int")
else:
self.selection_inds = inds
Expand All @@ -1728,9 +1731,9 @@ def on_select(self, verts):

def select_one(self, ind):
"""Select or deselect one sensor."""
if self.canvas._key == "shift":
if self.canvas._key == "control":
self.selection_inds = np.union1d(self.selection_inds, [ind])
elif self.canvas._key == "alt":
elif self.canvas._key == "ctrl+shift":
self.selection_inds = np.setdiff1d(self.selection_inds, [ind])
else:
return # don't notify()
Expand Down

0 comments on commit dbabf05

Please sign in to comment.