Skip to content

Commit aca4965

Browse files
wmvanvlietpre-commit-ci[bot]drammock
authored
Allow lasso selection sensors in a plot_evoked_topo (mne-tools#12071)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Daniel McCloy <[email protected]>
1 parent 6028982 commit aca4965

File tree

12 files changed

+348
-109
lines changed

12 files changed

+348
-109
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
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`_.

mne/epochs.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1353,6 +1353,7 @@ def plot_topo_image(
13531353
fig_facecolor="k",
13541354
fig_background=None,
13551355
font_color="w",
1356+
select=False,
13561357
show=True,
13571358
):
13581359
return plot_topo_image_epochs(
@@ -1371,6 +1372,7 @@ def plot_topo_image(
13711372
fig_facecolor=fig_facecolor,
13721373
fig_background=fig_background,
13731374
font_color=font_color,
1375+
select=select,
13741376
show=show,
13751377
)
13761378

mne/evoked.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -613,6 +613,7 @@ def plot_topo(
613613
background_color="w",
614614
noise_cov=None,
615615
exclude="bads",
616+
select=False,
616617
show=True,
617618
):
618619
""".
@@ -639,6 +640,7 @@ def plot_topo(
639640
background_color=background_color,
640641
noise_cov=noise_cov,
641642
exclude=exclude,
643+
select=select,
642644
show=show,
643645
)
644646

mne/viz/_figure.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -500,11 +500,11 @@ def _create_ch_location_fig(self, pick):
500500
show=False,
501501
)
502502
# highlight desired channel & disable interactivity
503-
inds = np.isin(fig.lasso.ch_names, [ch_name])
503+
fig.lasso.selection_inds = np.isin(fig.lasso.names, [ch_name])
504504
fig.lasso.disconnect()
505-
fig.lasso.alpha_other = 0.3
505+
fig.lasso.alpha_nonselected = 0.3
506506
fig.lasso.linewidth_selected = 3
507-
fig.lasso.style_sensors(inds)
507+
fig.lasso.style_objects()
508508

509509
return fig
510510

mne/viz/_mpl_figure.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1536,7 +1536,7 @@ def _update_selection(self):
15361536
def _update_highlighted_sensors(self):
15371537
"""Update the sensor plot to show what is selected."""
15381538
inds = np.isin(
1539-
self.mne.fig_selection.lasso.ch_names, self.mne.ch_names[self.mne.picks]
1539+
self.mne.fig_selection.lasso.names, self.mne.ch_names[self.mne.picks]
15401540
).nonzero()[0]
15411541
self.mne.fig_selection.lasso.select_many(inds)
15421542

mne/viz/evoked.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1153,6 +1153,7 @@ def plot_evoked_topo(
11531153
background_color="w",
11541154
noise_cov=None,
11551155
exclude="bads",
1156+
select=False,
11561157
show=True,
11571158
):
11581159
"""Plot 2D topography of evoked responses.
@@ -1218,6 +1219,15 @@ def plot_evoked_topo(
12181219
exclude : list of str | ``'bads'``
12191220
Channels names to exclude from the plot. If ``'bads'``, the
12201221
bad channels are excluded. By default, exclude is set to ``'bads'``.
1222+
select : bool
1223+
Whether to enable the lasso-selection tool to enable the user to select
1224+
channels. The selected channels will be available in
1225+
``fig.lasso.selection``.
1226+
1227+
.. versionadded:: 1.10.0
1228+
exclude : list of str | ``'bads'``
1229+
Channels names to exclude from the plot. If ``'bads'``, the
1230+
bad channels are excluded. By default, exclude is set to ``'bads'``.
12211231
show : bool
12221232
Show figure if True.
12231233
@@ -1274,10 +1284,11 @@ def plot_evoked_topo(
12741284
font_color=font_color,
12751285
merge_channels=merge_grads,
12761286
legend=legend,
1287+
noise_cov=noise_cov,
12771288
axes=axes,
12781289
exclude=exclude,
1290+
select=select,
12791291
show=show,
1280-
noise_cov=noise_cov,
12811292
)
12821293

12831294

mne/viz/tests/test_raw.py

Lines changed: 14 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1088,36 +1088,25 @@ def test_plot_sensors(raw):
10881088
pytest.raises(TypeError, plot_sensors, raw) # needs to be info
10891089
pytest.raises(ValueError, plot_sensors, raw.info, kind="sasaasd")
10901090
plt.close("all")
1091+
1092+
# Test lasso selection.
10911093
fig, sels = raw.plot_sensors("select", show_names=True)
10921094
ax = fig.axes[0]
1093-
1094-
# Click with no sensors
1095-
_fake_click(fig, ax, (0.0, 0.0), xform="data")
1096-
_fake_click(fig, ax, (0, 0.0), xform="data", kind="release")
1097-
assert fig.lasso.selection == []
1098-
1099-
# Lasso with 1 sensor (upper left)
1100-
_fake_click(fig, ax, (0, 1), xform="ax")
1101-
fig.canvas.draw()
1102-
assert fig.lasso.selection == []
1103-
_fake_click(fig, ax, (0.65, 1), xform="ax", kind="motion")
1104-
_fake_click(fig, ax, (0.65, 0.7), xform="ax", kind="motion")
1105-
_fake_keypress(fig, "control")
1106-
_fake_click(fig, ax, (0, 0.7), xform="ax", kind="release", key="control")
1095+
# Lasso a single sensor.
1096+
_fake_click(fig, ax, (-0.13, 0.13), xform="data")
1097+
_fake_click(fig, ax, (-0.11, 0.13), xform="data", kind="motion")
1098+
_fake_click(fig, ax, (-0.11, 0.06), xform="data", kind="motion")
1099+
_fake_click(fig, ax, (-0.13, 0.06), xform="data", kind="motion")
1100+
_fake_click(fig, ax, (-0.13, 0.13), xform="data", kind="motion")
1101+
_fake_click(fig, ax, (-0.13, 0.13), xform="data", kind="release")
11071102
assert fig.lasso.selection == ["MEG 0121"]
11081103

1109-
# check that point appearance changes
1110-
fc = fig.lasso.collection.get_facecolors()
1111-
ec = fig.lasso.collection.get_edgecolors()
1112-
assert (fc[:, -1] == [0.5, 1.0, 0.5]).all()
1113-
assert (ec[:, -1] == [0.25, 1.0, 0.25]).all()
1114-
1115-
_fake_click(fig, ax, (0.7, 1), xform="ax", kind="motion", key="control")
1116-
xy = ax.collections[0].get_offsets()
1117-
_fake_click(fig, ax, xy[2], xform="data", key="control") # single sel
1104+
# Add another sensor with a single click.
1105+
_fake_keypress(fig, "control")
1106+
_fake_click(fig, ax, (-0.1278, 0.0318), xform="data")
1107+
_fake_click(fig, ax, (-0.1278, 0.0318), xform="data", kind="release")
1108+
_fake_keypress(fig, "control", kind="release")
11181109
assert fig.lasso.selection == ["MEG 0121", "MEG 0131"]
1119-
_fake_click(fig, ax, xy[2], xform="data", key="control") # deselect
1120-
assert fig.lasso.selection == ["MEG 0121"]
11211110
plt.close("all")
11221111

11231112
raw.info["dev_head_t"] = None # like empty room

mne/viz/tests/test_topo.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
)
2424
from mne.viz.evoked import _line_plot_onselect
2525
from mne.viz.topo import _imshow_tfr, _plot_update_evoked_topo_proj, iter_topography
26-
from mne.viz.utils import _fake_click
26+
from mne.viz.utils import _fake_click, _fake_keypress
2727

2828
base_dir = Path(__file__).parents[2] / "io" / "tests" / "data"
2929
evoked_fname = base_dir / "test-ave.fif"
@@ -231,6 +231,16 @@ def test_plot_topo():
231231
break
232232
plt.close("all")
233233

234+
# Test plot_topo with selection of channels enabled.
235+
fig = evoked.plot_topo(select=True)
236+
ax = fig.axes[0]
237+
_fake_click(fig, ax, (0.05, 0.62), xform="data")
238+
_fake_click(fig, ax, (0.2, 0.62), xform="data", kind="motion")
239+
_fake_click(fig, ax, (0.2, 0.7), xform="data", kind="motion")
240+
_fake_click(fig, ax, (0.05, 0.7), xform="data", kind="motion")
241+
_fake_click(fig, ax, (0.05, 0.7), xform="data", kind="release")
242+
assert fig.lasso.selection == ["MEG 0113", "MEG 0112", "MEG 0111"]
243+
234244

235245
def test_plot_topo_nirs(fnirs_evoked):
236246
"""Test plotting of ERP topography for nirs data."""
@@ -296,6 +306,30 @@ def test_plot_topo_image_epochs():
296306
assert qm_cmap[0] is cmap
297307

298308

309+
def test_plot_topo_select():
310+
"""Test selecting sensors in an ERP topography plot."""
311+
# Show topography
312+
evoked = _get_epochs().average()
313+
fig = plot_evoked_topo(evoked, select=True)
314+
ax = fig.axes[0]
315+
316+
# Lasso select 3 out of the 6 sensors.
317+
_fake_click(fig, ax, (0.05, 0.5), xform="data")
318+
_fake_click(fig, ax, (0.2, 0.5), xform="data", kind="motion")
319+
_fake_click(fig, ax, (0.2, 0.6), xform="data", kind="motion")
320+
_fake_click(fig, ax, (0.05, 0.6), xform="data", kind="motion")
321+
_fake_click(fig, ax, (0.05, 0.5), xform="data", kind="motion")
322+
_fake_click(fig, ax, (0.05, 0.5), xform="data", kind="release")
323+
assert fig.lasso.selection == ["MEG 0132", "MEG 0133", "MEG 0131"]
324+
325+
# Add another sensor with a single click.
326+
_fake_keypress(fig, "control")
327+
_fake_click(fig, ax, (0.11, 0.65), xform="data")
328+
_fake_click(fig, ax, (0.21, 0.65), xform="data", kind="release")
329+
_fake_keypress(fig, "control", kind="release")
330+
assert fig.lasso.selection == ["MEG 0111", "MEG 0132", "MEG 0133", "MEG 0131"]
331+
332+
299333
def test_plot_tfr_topo():
300334
"""Test plotting of TFR data."""
301335
epochs = _get_epochs()

mne/viz/tests/test_utils.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from mne.viz import ClickableImage, add_background_image, mne_analyze_colormap
1717
from mne.viz.ui_events import ColormapRange, link, subscribe
1818
from mne.viz.utils import (
19+
SelectFromCollection,
1920
_compute_scalings,
2021
_fake_click,
2122
_fake_keypress,
@@ -274,3 +275,71 @@ def callback(event):
274275
cmap_new1 = fig.axes[0].CB.mappable.get_cmap().name
275276
cmap_new2 = fig2.axes[0].CB.mappable.get_cmap().name
276277
assert cmap_new1 == cmap_new2 == cmap_want != cmap_old
278+
279+
280+
def test_select_from_collection():
281+
"""Test the lasso selector for matplotlib figures."""
282+
fig, ax = plt.subplots()
283+
collection = ax.scatter([1, 2, 2, 1], [1, 1, 0, 0], color="black", edgecolor="red")
284+
ax.set_xlim(-1, 4)
285+
ax.set_ylim(-1, 2)
286+
lasso = SelectFromCollection(ax, collection, names=["A", "B", "C", "D"])
287+
assert lasso.selection == []
288+
289+
# Make a selection with no patches inside of it.
290+
_fake_click(fig, ax, (0, 0), xform="data")
291+
_fake_click(fig, ax, (0.5, 0), xform="data", kind="motion")
292+
_fake_click(fig, ax, (0.5, 1), xform="data", kind="motion")
293+
_fake_click(fig, ax, (0.5, 1), xform="data", kind="release")
294+
assert lasso.selection == []
295+
296+
# Doing a single click on a patch should not select it.
297+
_fake_click(fig, ax, (1, 1), xform="data")
298+
assert lasso.selection == []
299+
300+
# Make a selection with two patches in it.
301+
_fake_click(fig, ax, (0, 0.5), xform="data")
302+
_fake_click(fig, ax, (3, 0.5), xform="data", kind="motion")
303+
_fake_click(fig, ax, (3, 1.5), xform="data", kind="motion")
304+
_fake_click(fig, ax, (0, 1.5), xform="data", kind="motion")
305+
_fake_click(fig, ax, (0, 0.5), xform="data", kind="motion")
306+
_fake_click(fig, ax, (0, 0.5), xform="data", kind="release")
307+
assert lasso.selection == ["A", "B"]
308+
309+
# Use Control key to lasso an additional patch.
310+
_fake_keypress(fig, "control")
311+
_fake_click(fig, ax, (0.5, -0.5), xform="data")
312+
_fake_click(fig, ax, (1.5, -0.5), xform="data", kind="motion")
313+
_fake_click(fig, ax, (1.5, 0.5), xform="data", kind="motion")
314+
_fake_click(fig, ax, (0.5, 0.5), xform="data", kind="motion")
315+
_fake_click(fig, ax, (0.5, 0.5), xform="data", kind="release")
316+
_fake_keypress(fig, "control", kind="release")
317+
assert lasso.selection == ["A", "B", "D"]
318+
319+
# Use CTRL+SHIFT to remove a patch.
320+
_fake_keypress(fig, "ctrl+shift")
321+
_fake_click(fig, ax, (0.5, 0.5), xform="data")
322+
_fake_click(fig, ax, (1.5, 0.5), xform="data", kind="motion")
323+
_fake_click(fig, ax, (1.5, 1.5), xform="data", kind="motion")
324+
_fake_click(fig, ax, (0.5, 1.5), xform="data", kind="motion")
325+
_fake_click(fig, ax, (0.5, 1.5), xform="data", kind="release")
326+
_fake_keypress(fig, "ctrl+shift", kind="release")
327+
assert lasso.selection == ["B", "D"]
328+
329+
# Check that the two selected patches have a different appearance.
330+
fc = lasso.collection.get_facecolors()
331+
ec = lasso.collection.get_edgecolors()
332+
assert (fc[:, -1] == [0.5, 1.0, 0.5, 1.0]).all()
333+
assert (ec[:, -1] == [0.25, 1.0, 0.25, 1.0]).all()
334+
335+
# Test adding and removing single channels.
336+
lasso.select_one(2) # should not do anything without modifier keys
337+
assert lasso.selection == ["B", "D"]
338+
_fake_keypress(fig, "control")
339+
lasso.select_one(2) # add to selection
340+
_fake_keypress(fig, "control", kind="release")
341+
assert lasso.selection == ["B", "C", "D"]
342+
_fake_keypress(fig, "ctrl+shift")
343+
lasso.select_one(1) # remove from selection
344+
assert lasso.selection == ["C", "D"]
345+
_fake_keypress(fig, "ctrl+shift", kind="release")

0 commit comments

Comments
 (0)