From 549b77673073e00699b07915f6677c7b51fd3b79 Mon Sep 17 00:00:00 2001 From: Gnefil Date: Sun, 5 Jul 2026 00:35:01 +0800 Subject: [PATCH 01/21] ENH: add vertical marker in plot evoked that listens to timechange --- mne/viz/evoked.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/mne/viz/evoked.py b/mne/viz/evoked.py index 63aca378b59..b48ff42bf15 100644 --- a/mne/viz/evoked.py +++ b/mne/viz/evoked.py @@ -46,6 +46,7 @@ _set_contour_locator, plot_topomap, ) +from .ui_events import subscribe from .utils import ( DraggableColorbar, _check_cov, @@ -849,6 +850,17 @@ def _plot_lines( props=dict(alpha=0.5, facecolor="red"), ) + def on_time_change(event): + """Respond to a time change UI event.""" + for ax in np.array(axes)[selectables]: + if hasattr(ax, "_selectline") and ax._selectline is not None: + ax._selectline.remove() + + ax._selectline = ax.axvline(event.time, color="black", alpha=1) + ax.figure.canvas.draw() + + subscribe(fig, "time_change", on_time_change) + def _add_nave(ax, nave): """Add nave to axes.""" From 6a8101a5b764dd0925125c68bed45cd7bd10d569 Mon Sep 17 00:00:00 2001 From: Gnefil Date: Tue, 7 Jul 2026 13:30:59 +0800 Subject: [PATCH 02/21] ENH: Implement test for plot evoked timechange subscriber --- mne/viz/tests/test_evoked.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/mne/viz/tests/test_evoked.py b/mne/viz/tests/test_evoked.py index 5335f163bad..081600c9bc1 100644 --- a/mne/viz/tests/test_evoked.py +++ b/mne/viz/tests/test_evoked.py @@ -28,7 +28,7 @@ from mne.io import read_raw_fif from mne.stats.parametric import _parametric_ci from mne.utils import _record_warnings, catch_logging -from mne.viz import plot_compare_evokeds, plot_evoked_white +from mne.viz import plot_compare_evokeds, plot_evoked_white, ui_events from mne.viz.utils import _fake_click, _get_cmap base_dir = Path(__file__).parents[2] / "io" / "tests" / "data" @@ -230,6 +230,23 @@ def test_plot_evoked(): assert not np.all(np.isnan(line_clr) & (line_clr == 0)) +def test_plot_evoked_timechange(): + """Test that time change events are properly handled in plot_evoked.""" + epochs = _get_epochs() + evoked = epochs.average() + fig = evoked.plot(picks="mag") + ax = fig.axes[0] + + assert not hasattr(ax, "_selectline") + ui_events.publish(fig, ui_events.TimeChange(time=0.0)) + assert hasattr(ax, "_selectline") + assert ax._selectline.get_xdata()[0] == 0.0 + ui_events.publish(fig, ui_events.TimeChange(time=0.1)) + assert ax._selectline.get_xdata()[0] == 0.1 + + plt.close("all") + + def test_constrained_layout(): """Test that we handle constrained layouts correctly.""" fig, ax = plt.subplots(1, 1, layout="constrained") From b0d9e29c471611dd229f3bffdc108a5ccde5237b Mon Sep 17 00:00:00 2001 From: Gnefil Date: Tue, 7 Jul 2026 13:39:32 +0800 Subject: [PATCH 03/21] ENH: Document timechange listening behaviour --- mne/viz/evoked.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/mne/viz/evoked.py b/mne/viz/evoked.py index b48ff42bf15..bcd29ed2594 100644 --- a/mne/viz/evoked.py +++ b/mne/viz/evoked.py @@ -1123,6 +1123,14 @@ def plot_evoked( fig : instance of matplotlib.figure.Figure Figure containing the butterfly plots. + Notes + ----- + The figure will subscribe to the following UI events: + + * :class:`~mne.viz.ui_events.TimeChange` + + .. versionadded:: 1.13.0 + See Also -------- mne.viz.plot_evoked_white From 41933f527de7eb31b88c510fe5ddf5bfb9190df6 Mon Sep 17 00:00:00 2001 From: Gnefil Date: Tue, 7 Jul 2026 13:54:27 +0800 Subject: [PATCH 04/21] ENH: include non-selectable channels and improve logic --- mne/viz/evoked.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mne/viz/evoked.py b/mne/viz/evoked.py index bcd29ed2594..02467b35e7d 100644 --- a/mne/viz/evoked.py +++ b/mne/viz/evoked.py @@ -852,12 +852,12 @@ def _plot_lines( def on_time_change(event): """Respond to a time change UI event.""" - for ax in np.array(axes)[selectables]: - if hasattr(ax, "_selectline") and ax._selectline is not None: + for ax in np.array(axes): + if getattr(ax, "_selectline", None) is not None: ax._selectline.remove() ax._selectline = ax.axvline(event.time, color="black", alpha=1) - ax.figure.canvas.draw() + ax.figure.canvas.draw() subscribe(fig, "time_change", on_time_change) From 6029f36c8b1cd9f078ba48130681f7dfafa293fa Mon Sep 17 00:00:00 2001 From: Gnefil Date: Tue, 7 Jul 2026 15:24:03 +0800 Subject: [PATCH 05/21] BUG: Fix empty click after span select bug --- mne/viz/evoked.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mne/viz/evoked.py b/mne/viz/evoked.py index 02467b35e7d..f2a945a7491 100644 --- a/mne/viz/evoked.py +++ b/mne/viz/evoked.py @@ -133,6 +133,8 @@ def _line_plot_onselect( ch_types = [type_ for type_ in ch_types if type_ in ("eeg", "grad", "mag")] if len(ch_types) == 0: raise ValueError("Interactive topomaps only allowed for EEG and MEG channels.") + if xmin == xmax: + return if ( "grad" in ch_types and len(_pair_grad_sensors(info, topomap_coords=False, raise_error=False)) < 2 From f8b7524c5dd87b818f1700ce3e54cb717929e230 Mon Sep 17 00:00:00 2001 From: Gnefil Date: Tue, 7 Jul 2026 15:27:44 +0800 Subject: [PATCH 06/21] ENH: add publish timechange behaviour to plot_evoked --- mne/viz/evoked.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mne/viz/evoked.py b/mne/viz/evoked.py index f2a945a7491..7dab4a33633 100644 --- a/mne/viz/evoked.py +++ b/mne/viz/evoked.py @@ -46,7 +46,7 @@ _set_contour_locator, plot_topomap, ) -from .ui_events import subscribe +from .ui_events import TimeChange, publish, subscribe from .utils import ( DraggableColorbar, _check_cov, @@ -112,6 +112,8 @@ def _butterfly_on_button_press(event, params): event.canvas.draw() params["need_draw"] = False + publish(params["axes"][0].figure, TimeChange(event.xdata)) + def _line_plot_onselect( xmin, @@ -133,6 +135,7 @@ def _line_plot_onselect( ch_types = [type_ for type_ in ch_types if type_ in ("eeg", "grad", "mag")] if len(ch_types) == 0: raise ValueError("Interactive topomaps only allowed for EEG and MEG channels.") + # if xmin == xmax: return if ( From d695aa363ee550f41021a19cacbd5d9baf411d54 Mon Sep 17 00:00:00 2001 From: Gnefil Date: Tue, 7 Jul 2026 15:36:34 +0800 Subject: [PATCH 07/21] BUG: Fix spatial colors not defaulted to auto bug --- mne/viz/evoked.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mne/viz/evoked.py b/mne/viz/evoked.py index 7dab4a33633..43cf00b0888 100644 --- a/mne/viz/evoked.py +++ b/mne/viz/evoked.py @@ -1003,7 +1003,7 @@ def plot_evoked( axes=None, gfp=False, window_title=None, - spatial_colors=False, + spatial_colors="auto", zorder="unsorted", selectable=True, noise_cov=None, From dfb08a1d484515c117a8073c7abd93f3a2e3f5dc Mon Sep 17 00:00:00 2001 From: Gnefil Date: Wed, 8 Jul 2026 17:03:42 +0800 Subject: [PATCH 08/21] BUG: Add comments for the fix --- mne/viz/evoked.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mne/viz/evoked.py b/mne/viz/evoked.py index 43cf00b0888..1b1d5c15c4b 100644 --- a/mne/viz/evoked.py +++ b/mne/viz/evoked.py @@ -135,7 +135,7 @@ def _line_plot_onselect( ch_types = [type_ for type_ in ch_types if type_ in ("eeg", "grad", "mag")] if len(ch_types) == 0: raise ValueError("Interactive topomaps only allowed for EEG and MEG channels.") - # + # First click after SpanSelector triggers this again, so reject when zero-width span if xmin == xmax: return if ( From d7d09cc7bc58ae8e8524895d973bc5e162c1240d Mon Sep 17 00:00:00 2001 From: Gnefil Date: Wed, 8 Jul 2026 19:06:06 +0800 Subject: [PATCH 09/21] ENH: Fix publish only when click is in axes --- mne/viz/evoked.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/mne/viz/evoked.py b/mne/viz/evoked.py index 1b1d5c15c4b..ebfb55be6ff 100644 --- a/mne/viz/evoked.py +++ b/mne/viz/evoked.py @@ -112,7 +112,11 @@ def _butterfly_on_button_press(event, params): event.canvas.draw() params["need_draw"] = False - publish(params["axes"][0].figure, TimeChange(event.xdata)) + idx = np.where([event.inaxes is ax for ax in params["axes"]])[0] + for ax in params["axes"]: + if event.inaxes is ax: + publish(ax.figure, TimeChange(event.xdata)) + break def _line_plot_onselect( From 5a216cde0e52c142f7f93d8182c30b6ff0a609fb Mon Sep 17 00:00:00 2001 From: Gnefil Date: Wed, 8 Jul 2026 19:07:32 +0800 Subject: [PATCH 10/21] ENH: Test implemented functions and pass --- mne/viz/tests/test_evoked.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/mne/viz/tests/test_evoked.py b/mne/viz/tests/test_evoked.py index 081600c9bc1..f0a725651e5 100644 --- a/mne/viz/tests/test_evoked.py +++ b/mne/viz/tests/test_evoked.py @@ -229,6 +229,22 @@ def test_plot_evoked(): line_clr = [x.get_color() for x in fig.axes[0].get_lines()] assert not np.all(np.isnan(line_clr) & (line_clr == 0)) + # test time selection by clicking on the plot + epochs = _get_epochs() + evoked = epochs.average() + fig = evoked.plot(picks="mag") + ax = fig.axes[0] + + assert not hasattr(ax, "_selectline") + _fake_click(fig, ax, (0.5, 0.5), kind="press") + assert hasattr(ax, "_selectline") + assert ax._selectline.get_xdata()[0] == 0.0 + + _fake_click(fig, ax, (0.6, 0.5), kind="press") + assert ax._selectline.get_xdata()[0] != 0.0 + + plt.close("all") + def test_plot_evoked_timechange(): """Test that time change events are properly handled in plot_evoked.""" @@ -237,10 +253,9 @@ def test_plot_evoked_timechange(): fig = evoked.plot(picks="mag") ax = fig.axes[0] - assert not hasattr(ax, "_selectline") - ui_events.publish(fig, ui_events.TimeChange(time=0.0)) - assert hasattr(ax, "_selectline") + _fake_click(fig, ax, (0.5, 0.5), kind="press") assert ax._selectline.get_xdata()[0] == 0.0 + ui_events.publish(fig, ui_events.TimeChange(time=0.1)) assert ax._selectline.get_xdata()[0] == 0.1 From c9ec1f903e3640c12b64d236fb79e78a2381f033 Mon Sep 17 00:00:00 2001 From: Gnefil Date: Wed, 8 Jul 2026 19:08:26 +0800 Subject: [PATCH 11/21] ENH: Document publish behaviour in figure too --- mne/viz/evoked.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mne/viz/evoked.py b/mne/viz/evoked.py index ebfb55be6ff..69f5ee32719 100644 --- a/mne/viz/evoked.py +++ b/mne/viz/evoked.py @@ -1134,7 +1134,7 @@ def plot_evoked( Notes ----- - The figure will subscribe to the following UI events: + The figure will publish and subscribe to the following UI events: * :class:`~mne.viz.ui_events.TimeChange` From 7d1dd9e229b37cd008605ffa7b1f3b2aa1ba9d6c Mon Sep 17 00:00:00 2001 From: Gnefil Date: Thu, 9 Jul 2026 01:40:11 +0800 Subject: [PATCH 12/21] ENH: Add changelogs --- doc/changes/dev/14025.bugfix.rst | 2 ++ doc/changes/dev/14025.newfeature.rst | 1 + 2 files changed, 3 insertions(+) create mode 100644 doc/changes/dev/14025.bugfix.rst create mode 100644 doc/changes/dev/14025.newfeature.rst diff --git a/doc/changes/dev/14025.bugfix.rst b/doc/changes/dev/14025.bugfix.rst new file mode 100644 index 00000000000..3d6c09a78dc --- /dev/null +++ b/doc/changes/dev/14025.bugfix.rst @@ -0,0 +1,2 @@ +Fix `spatial_colors` is not "auto" as described by docstring in :func:`mne.viz.plot_evoked`. +Fix empty topomap created when clicking after time range selection in :func:`mne.viz.plot_evoked`, by `Lifeng Qiu Lin`_. \ No newline at end of file diff --git a/doc/changes/dev/14025.newfeature.rst b/doc/changes/dev/14025.newfeature.rst new file mode 100644 index 00000000000..3d27d957fd9 --- /dev/null +++ b/doc/changes/dev/14025.newfeature.rst @@ -0,0 +1 @@ +Add ``TimeChange`` behaviour for :func:`mne.viz.plot_evoked`, where time is marked with solid vertical line, by `Lifeng Qiu Lin`_. \ No newline at end of file From 8c6f9db02ffb49d0a4cac5d19741b37f894315d2 Mon Sep 17 00:00:00 2001 From: Gnefil Date: Thu, 9 Jul 2026 11:45:35 +0800 Subject: [PATCH 13/21] ENH: Swap the order of docstring sections --- mne/viz/evoked.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mne/viz/evoked.py b/mne/viz/evoked.py index 69f5ee32719..c5ed5222e26 100644 --- a/mne/viz/evoked.py +++ b/mne/viz/evoked.py @@ -1132,6 +1132,10 @@ def plot_evoked( fig : instance of matplotlib.figure.Figure Figure containing the butterfly plots. + See Also + -------- + mne.viz.plot_evoked_white + Notes ----- The figure will publish and subscribe to the following UI events: @@ -1139,10 +1143,6 @@ def plot_evoked( * :class:`~mne.viz.ui_events.TimeChange` .. versionadded:: 1.13.0 - - See Also - -------- - mne.viz.plot_evoked_white """ # noqa: E501 return _plot_evoked( evoked=evoked, From fb2acc98b154e0826479826908eced595b9a5139 Mon Sep 17 00:00:00 2001 From: Gnefil Date: Thu, 9 Jul 2026 11:59:36 +0800 Subject: [PATCH 14/21] ENH: Improve code style --- mne/viz/evoked.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/mne/viz/evoked.py b/mne/viz/evoked.py index c5ed5222e26..d5948931e41 100644 --- a/mne/viz/evoked.py +++ b/mne/viz/evoked.py @@ -112,10 +112,9 @@ def _butterfly_on_button_press(event, params): event.canvas.draw() params["need_draw"] = False - idx = np.where([event.inaxes is ax for ax in params["axes"]])[0] for ax in params["axes"]: if event.inaxes is ax: - publish(ax.figure, TimeChange(event.xdata)) + publish(ax.figure, TimeChange(time=event.xdata)) break @@ -861,11 +860,12 @@ def _plot_lines( def on_time_change(event): """Respond to a time change UI event.""" - for ax in np.array(axes): - if getattr(ax, "_selectline", None) is not None: - ax._selectline.remove() - - ax._selectline = ax.axvline(event.time, color="black", alpha=1) + for ax in axes: + line = getattr(ax, "_selectline", None) + if line is None: + ax._selectline = ax.axvline(event.time, color="black", alpha=1) + else: + line.set_xdata([event.time, event.time]) ax.figure.canvas.draw() subscribe(fig, "time_change", on_time_change) From 2d82e6c516b044c572bc175f98924bea4587b4c6 Mon Sep 17 00:00:00 2001 From: Lifeng <76589235+Gnefil@users.noreply.github.com> Date: Tue, 14 Jul 2026 10:26:31 +0800 Subject: [PATCH 15/21] Update doc/changes/dev/14025.bugfix.rst Co-authored-by: Marijn van Vliet --- doc/changes/dev/14025.bugfix.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/changes/dev/14025.bugfix.rst b/doc/changes/dev/14025.bugfix.rst index 3d6c09a78dc..87bb2fa5676 100644 --- a/doc/changes/dev/14025.bugfix.rst +++ b/doc/changes/dev/14025.bugfix.rst @@ -1,2 +1,2 @@ -Fix `spatial_colors` is not "auto" as described by docstring in :func:`mne.viz.plot_evoked`. +Fix ``spatial_colors`` is not ``"auto"`` as described by docstring in :func:`mne.viz.plot_evoked`. Fix empty topomap created when clicking after time range selection in :func:`mne.viz.plot_evoked`, by `Lifeng Qiu Lin`_. \ No newline at end of file From 255d95610daf0d16d7fa6d1b5c2fe3932045f6e3 Mon Sep 17 00:00:00 2001 From: Gnefil Date: Thu, 16 Jul 2026 11:02:18 +0200 Subject: [PATCH 16/21] ENH: Add hover effect for time selection --- mne/viz/evoked.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/mne/viz/evoked.py b/mne/viz/evoked.py index d5948931e41..e7b97649fe7 100644 --- a/mne/viz/evoked.py +++ b/mne/viz/evoked.py @@ -644,7 +644,27 @@ def _plot_lines( need_draw=False, path_effects=path_effects, ) - fig.canvas.mpl_connect("pick_event", partial(_butterfly_onpick, params=params)) + + def _cursor_vline(event): + if not event.inaxes: + return + for ax in axes: + line = getattr(ax, "_cursorline", None) + if line is None: + ax._cursorline = ax.axvline(event.xdata, color="black", alpha=0.2) + else: + line.set_xdata([event.xdata, event.xdata]) + ax.figure.canvas.draw() + + def _rm_cursor(event): + for ax in axes: + if ax._cursorline is not None: + ax._cursorline.remove() + ax._cursorline = None + ax.figure.canvas.draw() + + fig.canvas.mpl_connect("motion_notify_event", _cursor_vline) + fig.canvas.mpl_connect("figure_leave_event", _rm_cursor) fig.canvas.mpl_connect( "button_press_event", partial(_butterfly_on_button_press, params=params) ) From 14e5de963d778650d29aa8a366587ff626844163 Mon Sep 17 00:00:00 2001 From: Gnefil Date: Fri, 17 Jul 2026 11:30:36 +0200 Subject: [PATCH 17/21] ENH: Remove existing channel pick behaviour and implement hover for channels --- mne/viz/evoked.py | 103 ++++++++++++++++++---------------------------- 1 file changed, 41 insertions(+), 62 deletions(-) diff --git a/mne/viz/evoked.py b/mne/viz/evoked.py index e7b97649fe7..8aa9d39e1e3 100644 --- a/mne/viz/evoked.py +++ b/mne/viz/evoked.py @@ -73,51 +73,6 @@ ) -def _butterfly_onpick(event, params): - """Add a channel name on click.""" - params["need_draw"] = True - ax = event.artist.axes - ax_idx = np.where([ax is a for a in params["axes"]])[0] - if len(ax_idx) == 0: # this can happen if ax param is used - return # let the other axes handle it - else: - ax_idx = ax_idx[0] - lidx = np.where([line is event.artist for line in params["lines"][ax_idx]])[0][0] - ch_name = params["ch_names"][params["idxs"][ax_idx][lidx]] - text = params["texts"][ax_idx] - x = event.artist.get_xdata()[event.ind[0]] - y = event.artist.get_ydata()[event.ind[0]] - text.set_x(x) - text.set_y(y) - text.set_text(ch_name) - text.set_color(event.artist.get_color()) - text.set_alpha(1.0) - text.set_zorder(len(ax.lines)) # to make sure it goes on top of the lines - text.set_path_effects(params["path_effects"]) - # do NOT redraw here, since for butterfly plots hundreds of lines could - # potentially be picked -- use on_button_press (happens once per click) - # to do the drawing - - -def _butterfly_on_button_press(event, params): - """Only draw once for picking.""" - if params["need_draw"]: - event.canvas.draw() - else: - idx = np.where([event.inaxes is ax for ax in params["axes"]])[0] - if len(idx) == 1: - text = params["texts"][idx[0]] - text.set_alpha(0.0) - text.set_path_effects([]) - event.canvas.draw() - params["need_draw"] = False - - for ax in params["axes"]: - if event.inaxes is ax: - publish(ax.figure, TimeChange(time=event.xdata)) - break - - def _line_plot_onselect( xmin, xmax, @@ -634,40 +589,64 @@ def _plot_lines( selectables[type_idx] = False if selectable: - # Parameters for butterfly interactive plots - params = dict( - axes=axes, - texts=texts, - lines=lines, - ch_names=info["ch_names"], - idxs=idxs, - need_draw=False, - path_effects=path_effects, - ) - def _cursor_vline(event): + def _on_hover(event): if not event.inaxes: return + + # pop up channel name on hover + ax = event.inaxes + ax_idx = np.where([ax is a for a in axes])[0] + if len(ax_idx): # do nothing if ax is used instead + ax_idx = ax_idx[0] + text = texts[ax_idx] + hovered = None + for line_idx, line in enumerate(lines[ax_idx]): + hit, details = line.contains(event) + if hit: + hovered = (line_idx, line, details["ind"][0]) + break # stop recursion at first hit + if hovered is not None: + line_idx, line, ind = hovered + ch_name = info["ch_names"][idxs[ax_idx][line_idx]] + text.set_position((line.get_xdata()[ind], line.get_ydata()[ind])) + text.set_text(ch_name) + text.set_color(line.get_color()) + text.set_alpha(1.0) + text.set_zorder( + len(ax.lines) + ) # to make sure it goes on top of the lines + text.set_path_effects(path_effects) + else: + text.set_alpha(0.0) + text.set_path_effects(path_effects) + + # vertical line to indicate time point for ax in axes: line = getattr(ax, "_cursorline", None) if line is None: ax._cursorline = ax.axvline(event.xdata, color="black", alpha=0.2) else: line.set_xdata([event.xdata, event.xdata]) - ax.figure.canvas.draw() + ax.figure.canvas.draw_idle() def _rm_cursor(event): for ax in axes: if ax._cursorline is not None: ax._cursorline.remove() ax._cursorline = None - ax.figure.canvas.draw() + ax.figure.canvas.draw_idle() + + def _select_time(event): + for ax in axes: + if event.inaxes is ax: + publish(ax.figure, TimeChange(time=event.xdata)) + break - fig.canvas.mpl_connect("motion_notify_event", _cursor_vline) + fig.canvas.mpl_connect("motion_notify_event", _on_hover) fig.canvas.mpl_connect("figure_leave_event", _rm_cursor) - fig.canvas.mpl_connect( - "button_press_event", partial(_butterfly_on_button_press, params=params) - ) + fig.canvas.mpl_connect("button_press_event", _select_time) + for ai, (ax, this_type) in enumerate(zip(axes, ch_types_used)): line_list = list() # 'line_list' contains the lines for this axes if unit is False: From e2c46d878e82a3e90e6d5376c3afb6a4290d692b Mon Sep 17 00:00:00 2001 From: Gnefil Date: Fri, 17 Jul 2026 19:33:07 +0200 Subject: [PATCH 18/21] FIX: Add back butterfly auxiliary functions because mne.viz.ica._plot_ica_sources_evoked uses it --- mne/viz/evoked.py | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/mne/viz/evoked.py b/mne/viz/evoked.py index 8aa9d39e1e3..bb71a8bb7e2 100644 --- a/mne/viz/evoked.py +++ b/mne/viz/evoked.py @@ -73,6 +73,51 @@ ) +def _butterfly_onpick(event, params): + """Add a channel name on click.""" + params["need_draw"] = True + ax = event.artist.axes + ax_idx = np.where([ax is a for a in params["axes"]])[0] + if len(ax_idx) == 0: # this can happen if ax param is used + return # let the other axes handle it + else: + ax_idx = ax_idx[0] + lidx = np.where([line is event.artist for line in params["lines"][ax_idx]])[0][0] + ch_name = params["ch_names"][params["idxs"][ax_idx][lidx]] + text = params["texts"][ax_idx] + x = event.artist.get_xdata()[event.ind[0]] + y = event.artist.get_ydata()[event.ind[0]] + text.set_x(x) + text.set_y(y) + text.set_text(ch_name) + text.set_color(event.artist.get_color()) + text.set_alpha(1.0) + text.set_zorder(len(ax.lines)) # to make sure it goes on top of the lines + text.set_path_effects(params["path_effects"]) + # do NOT redraw here, since for butterfly plots hundreds of lines could + # potentially be picked -- use on_button_press (happens once per click) + # to do the drawing + + +def _butterfly_on_button_press(event, params): + """Only draw once for picking.""" + if params["need_draw"]: + event.canvas.draw() + else: + idx = np.where([event.inaxes is ax for ax in params["axes"]])[0] + if len(idx) == 1: + text = params["texts"][idx[0]] + text.set_alpha(0.0) + text.set_path_effects([]) + event.canvas.draw() + params["need_draw"] = False + + for ax in params["axes"]: + if event.inaxes is ax: + publish(ax.figure, TimeChange(time=event.xdata)) + break + + def _line_plot_onselect( xmin, xmax, From 93cfdfdbb3d66aca2ce2311195ba4e5e3e718095 Mon Sep 17 00:00:00 2001 From: Gnefil Date: Sat, 18 Jul 2026 18:21:52 +0200 Subject: [PATCH 19/21] FIX: Clear path effect on hover release --- mne/viz/evoked.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mne/viz/evoked.py b/mne/viz/evoked.py index bb71a8bb7e2..9098ca168a4 100644 --- a/mne/viz/evoked.py +++ b/mne/viz/evoked.py @@ -664,7 +664,7 @@ def _on_hover(event): text.set_path_effects(path_effects) else: text.set_alpha(0.0) - text.set_path_effects(path_effects) + text.set_path_effects([]) # vertical line to indicate time point for ax in axes: From 991f13f2086dfeb770bda91bd5d2f3ee044b358d Mon Sep 17 00:00:00 2001 From: Gnefil Date: Sat, 18 Jul 2026 18:47:10 +0200 Subject: [PATCH 20/21] ENH: Add test to validate hover and press behaviour --- mne/viz/tests/test_evoked.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/mne/viz/tests/test_evoked.py b/mne/viz/tests/test_evoked.py index f0a725651e5..c835f10695a 100644 --- a/mne/viz/tests/test_evoked.py +++ b/mne/viz/tests/test_evoked.py @@ -229,12 +229,33 @@ def test_plot_evoked(): line_clr = [x.get_color() for x in fig.axes[0].get_lines()] assert not np.all(np.isnan(line_clr) & (line_clr == 0)) - # test time selection by clicking on the plot + # test hover and click functions epochs = _get_epochs() evoked = epochs.average() fig = evoked.plot(picks="mag") ax = fig.axes[0] + # test hover + # check cursorline is created on hover + assert not hasattr(ax, "_cursorline") + _fake_click(fig, ax, (0.5, 0.5), kind="motion") + assert hasattr(ax, "_cursorline") + assert ax._cursorline.get_xdata()[0] == 0.0 + # check if text of channel name is displayed due to hover + text = ax.texts[0] + assert text.get_alpha() == 0.0 + traces = [ + line + for line in ax.get_lines() + if line.get_picker() and len(line.get_xdata()) > 3 + ] + trace = traces[0] # pick the first trace (MEG 0111) + x, y = trace.get_xdata(), trace.get_ydata() + i = len(x) // 2 + _fake_click(fig, ax, (x[i], y[i]), xform="data", kind="motion") + assert text.get_alpha() == 1.0 + + # test click assert not hasattr(ax, "_selectline") _fake_click(fig, ax, (0.5, 0.5), kind="press") assert hasattr(ax, "_selectline") From 9cf4faaf98b73ebb965a7837877292c20223eee3 Mon Sep 17 00:00:00 2001 From: Gnefil Date: Sat, 18 Jul 2026 18:50:01 +0200 Subject: [PATCH 21/21] DOC: Update changelog --- doc/changes/dev/14025.newfeature.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/changes/dev/14025.newfeature.rst b/doc/changes/dev/14025.newfeature.rst index 3d27d957fd9..dcbcd8b77b1 100644 --- a/doc/changes/dev/14025.newfeature.rst +++ b/doc/changes/dev/14025.newfeature.rst @@ -1 +1 @@ -Add ``TimeChange`` behaviour for :func:`mne.viz.plot_evoked`, where time is marked with solid vertical line, by `Lifeng Qiu Lin`_. \ No newline at end of file +Add ``TimeChange`` behaviour for :func:`mne.viz.plot_evoked`, where vertical time cursor is previewed on hover and marked on click. Also change previous channel names pop-up on click to on hover, by `Lifeng Qiu Lin`_. \ No newline at end of file