From a62f771f00bce93b8c5807e0a995343904fc1b7a Mon Sep 17 00:00:00 2001 From: Gnefil Date: Tue, 16 Jun 2026 13:34:40 +0800 Subject: [PATCH 01/18] Add link from evoked_topo subfigures to other figures --- mne/viz/topo.py | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/mne/viz/topo.py b/mne/viz/topo.py index 5c43d4de48e..23b62645c41 100644 --- a/mne/viz/topo.py +++ b/mne/viz/topo.py @@ -13,7 +13,7 @@ from .._fiff.pick import _picks_to_idx, channel_type, pick_types from ..defaults import _handle_default from ..utils import Bunch, _check_option, _clean_names, _is_numeric, _to_rgb, fill_doc -from .ui_events import ChannelsSelect, publish, subscribe +from .ui_events import ChannelsSelect, TimeChange, publish, subscribe from .utils import ( DraggableColorbar, SelectFromCollection, @@ -413,14 +413,14 @@ def _plot_topo_onpick(event, show_func): return ch_idx = orig_ax._mne_ch_idx face_color = orig_ax._mne_ax_face_color - fig, ax = plt.subplots(1) + subfig, ax = plt.subplots(1) plt.title(orig_ax._mne_ch_name) ax.set_facecolor(face_color) # allow custom function to override parameters - show_func(ax, ch_idx) - plt_show(fig=fig) + show_func(ax, ch_idx, orig_fig=fig) + plt_show(fig=subfig) except Exception as err: # matplotlib silently ignores exceptions in event handlers, @@ -568,6 +568,7 @@ def _plot_timeseries( hline=None, hvline_color="w", labels=None, + orig_fig=None, ): """Show time series on topo split across multiple axes.""" import matplotlib.pyplot as plt @@ -637,6 +638,11 @@ def _rm_cursor(event): ax._cursorline = None ax.figure.canvas.draw() + def _on_click(event): + if event.inaxes == ax: + publish(ax.figure, TimeChange(time=event.xdata)) + publish(orig_fig, TimeChange(time=event.xdata)) + ax._cursorline = None # choose cursor color based on perceived brightness of background facecol = _to_rgb(ax.get_facecolor()) @@ -645,6 +651,7 @@ def _rm_cursor(event): plt.connect("motion_notify_event", _cursor_vline) plt.connect("axes_leave_event", _rm_cursor) + plt.connect("button_press_event", _on_click) ymin, ymax = ax.get_ylim() # don't pass vline or hline here (this fxn doesn't do hvline_color): @@ -1148,6 +1155,8 @@ def _plot_evoked_topo( select=select, ) + subscribe(fig, "time_change", partial(on_time_change, fig=fig)) + add_background_image(fig, fig_background) if legend is not False: @@ -1181,6 +1190,12 @@ def _plot_evoked_topo( return fig +def on_time_change(event, fig): + """Respond to a time change UI event. Currently only prints.""" + print(len(fig.axes), len(fig.axes[0].lines)) + print("Event triggered:", event) + + def _plot_update_evoked_topo_proj(params, bools): """Update topo sensor plots.""" evokeds = [e.copy() for e in params["evokeds"]] From 9aa4ba6248a4620eff53d685ae3149a22d137863 Mon Sep 17 00:00:00 2001 From: Gnefil Date: Thu, 18 Jun 2026 21:39:28 +0800 Subject: [PATCH 02/18] ENH: add merge linking option which allows evoked topo timechange to update time in subfigures synchronously --- mne/viz/topo.py | 36 ++++++++++++++++++++++++++++++------ mne/viz/ui_events.py | 16 ++++++++++++++-- 2 files changed, 44 insertions(+), 8 deletions(-) diff --git a/mne/viz/topo.py b/mne/viz/topo.py index 23b62645c41..7cfde45f5f5 100644 --- a/mne/viz/topo.py +++ b/mne/viz/topo.py @@ -13,7 +13,13 @@ from .._fiff.pick import _picks_to_idx, channel_type, pick_types from ..defaults import _handle_default from ..utils import Bunch, _check_option, _clean_names, _is_numeric, _to_rgb, fill_doc -from .ui_events import ChannelsSelect, TimeChange, publish, subscribe +from .ui_events import ( + ChannelsSelect, + TimeChange, + link, + publish, + subscribe, +) from .utils import ( DraggableColorbar, SelectFromCollection, @@ -628,7 +634,7 @@ def _cursor_vline(event): return if ax._cursorline is not None: ax._cursorline.remove() - ax._cursorline = ax.axvline(event.xdata, color=ax._cursorcolor) + ax._cursorline = ax.axvline(event.xdata, color=ax._cursorcolor, alpha=0.2) ax.figure.canvas.draw() def _rm_cursor(event): @@ -641,7 +647,16 @@ def _rm_cursor(event): def _on_click(event): if event.inaxes == ax: publish(ax.figure, TimeChange(time=event.xdata)) - publish(orig_fig, TimeChange(time=event.xdata)) + _update_selectline(event.xdata) + + def _on_time_change_sub(event): + _update_selectline(event.time) + + def _update_selectline(time): + if ax._selectline is not None: + ax._selectline.remove() + ax._selectline = ax.axvline(time, color=ax._selectcolor, alpha=1) + ax.figure.canvas.draw() ax._cursorline = None # choose cursor color based on perceived brightness of background @@ -649,10 +664,18 @@ def _on_click(event): face_brightness = np.dot(facecol, [299, 587, 114]) ax._cursorcolor = "white" if face_brightness < 150 else "black" + # setattr(orig_fig, "_current_time", None) + ax._selectline = None + ax._selectcolor = "white" if face_brightness < 150 else "black" + plt.connect("motion_notify_event", _cursor_vline) plt.connect("axes_leave_event", _rm_cursor) plt.connect("button_press_event", _on_click) + subscribe(ax.figure, "time_change", _on_time_change_sub) + + link(orig_fig, ax.figure, merge=True) + ymin, ymax = ax.get_ylim() # don't pass vline or hline here (this fxn doesn't do hvline_color): _setup_ax_spines(ax, [], tmin, tmax, ymin, ymax, hline=False) @@ -1155,6 +1178,8 @@ def _plot_evoked_topo( select=select, ) + setattr(fig, "_current_time", None) + subscribe(fig, "time_change", partial(on_time_change, fig=fig)) add_background_image(fig, fig_background) @@ -1191,9 +1216,8 @@ def _plot_evoked_topo( def on_time_change(event, fig): - """Respond to a time change UI event. Currently only prints.""" - print(len(fig.axes), len(fig.axes[0].lines)) - print("Event triggered:", event) + """Respond to a time change UI event.""" + fig._current_time = event.time def _plot_update_evoked_topo_proj(params, bools): diff --git a/mne/viz/ui_events.py b/mne/viz/ui_events.py index 5e555ca6a9d..6b0986a7076 100644 --- a/mne/viz/ui_events.py +++ b/mne/viz/ui_events.py @@ -407,7 +407,7 @@ def unsubscribe(fig, event_names, callback=None, *, verbose=None): @verbose -def link(*figs, include_events=None, exclude_events=None, verbose=None): +def link(*figs, include_events=None, exclude_events=None, merge=False, verbose=None): """Link the event channels of two figures together. When event channels are linked, any events that are published on one @@ -426,6 +426,9 @@ def link(*figs, include_events=None, exclude_events=None, verbose=None): exclude_events : list of str | None Select which events not to publish across figures. By default (``None``), no events are excluded. + merge : bool + If ``True``, also link the existing link-groups that figs already belong + to, so all members are mutually linked. %(verbose)s """ if include_events is not None: @@ -439,7 +442,16 @@ def link(*figs, include_events=None, exclude_events=None, verbose=None): if fig not in _event_channel_links: _event_channel_links[fig] = weakref.WeakKeyDictionary() - # Link the event channels + # Merge link groups that the figures already belong + if merge: + figs_set = weakref.WeakSet() + for fig in figs: + figs_set.add(fig) + for linked_fig in _event_channel_links[fig].keys(): + figs_set.add(linked_fig) + # Deliberately let current include and exclude events dominate over past ones + figs = figs_set + # Link the event channels plainly for fig1 in figs: for fig2 in figs: if fig1 is not fig2: From 59708b0dd92b28e0a4ec20523c42752602243385 Mon Sep 17 00:00:00 2001 From: Gnefil Date: Sun, 21 Jun 2026 15:04:14 +0800 Subject: [PATCH 03/18] ENH: add test for merge function in link --- mne/viz/tests/test_ui_events.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/mne/viz/tests/test_ui_events.py b/mne/viz/tests/test_ui_events.py index a2be4445ff9..db4cd7253d5 100644 --- a/mne/viz/tests/test_ui_events.py +++ b/mne/viz/tests/test_ui_events.py @@ -234,9 +234,18 @@ def callback(event): ui_events.publish(fig2, ui_events.TimeChange(time=10.2)) assert len(callback_calls) == 2 # Only called for both figures once + # Test merge link groups + fig3 = plt.figure() + ui_events.subscribe(fig3, "time_change", callback) + ui_events.link(fig2, fig3, merge=True) # fig1 and fig2 are already linked + callback_calls.clear() + ui_events.publish(fig3, ui_events.TimeChange(time=10.2)) + assert len(callback_calls) == 3 # Called for all three figures + # Test cleanup fig1.canvas.callbacks.process("close_event", None) fig2.canvas.callbacks.process("close_event", None) + fig3.canvas.callbacks.process("close_event", None) assert len(event_channels) == 0 assert len(event_channel_links) == 0 From 563d89a5f9f3bc70fa9b7ce374b72c103a13591b Mon Sep 17 00:00:00 2001 From: Gnefil Date: Sun, 21 Jun 2026 15:08:34 +0800 Subject: [PATCH 04/18] ENH: improve link test with bidirection --- mne/viz/tests/test_ui_events.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mne/viz/tests/test_ui_events.py b/mne/viz/tests/test_ui_events.py index db4cd7253d5..5b99aebacaa 100644 --- a/mne/viz/tests/test_ui_events.py +++ b/mne/viz/tests/test_ui_events.py @@ -240,7 +240,8 @@ def callback(event): ui_events.link(fig2, fig3, merge=True) # fig1 and fig2 are already linked callback_calls.clear() ui_events.publish(fig3, ui_events.TimeChange(time=10.2)) - assert len(callback_calls) == 3 # Called for all three figures + ui_events.publish(fig1, ui_events.TimeChange(time=10.2)) + assert len(callback_calls) == 6 # Called for all three figures twice # Test cleanup fig1.canvas.callbacks.process("close_event", None) From 1c04c91ab4b6fce9f281175a8ab308886a9e4485 Mon Sep 17 00:00:00 2001 From: Gnefil Date: Sun, 21 Jun 2026 17:15:40 +0800 Subject: [PATCH 05/18] ENH: add test for evoked_topo time change --- mne/viz/tests/test_topo.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/mne/viz/tests/test_topo.py b/mne/viz/tests/test_topo.py index fe6c938d244..d5fde725246 100644 --- a/mne/viz/tests/test_topo.py +++ b/mne/viz/tests/test_topo.py @@ -25,6 +25,7 @@ mne_analyze_colormap, plot_evoked_topo, plot_topo_image_epochs, + ui_events, ) from mne.viz.evoked import _line_plot_onselect from mne.viz.topo import _imshow_tfr, _plot_update_evoked_topo_proj, iter_topography @@ -286,6 +287,16 @@ def test_plot_topo_single_ch(): assert isinstance(ax._cursorline, matplotlib.lines.Line2D) _fake_click(fig, ax, (1.5, 1.5), kind="motion") # cursor should disappear assert ax._cursorline is None + # test select bar + _fake_click(fig, ax, (1.5, 1.5), kind="press") + assert ax._selectline is None + _fake_click(fig, ax, (0.5, 0.5), kind="press") + assert isinstance(ax._selectline, matplotlib.lines.Line2D) + init_time = ax._selectline.get_xdata()[0] + assert init_time == 0.0 + _fake_click(fig, ax, (0.6, 0.5), kind="press") + changed_time = ax._selectline.get_xdata()[0] + assert changed_time != init_time plt.close("all") @@ -346,6 +357,32 @@ def test_plot_topo_select(): assert fig.lasso.selection == ["MEG 0111", "MEG 0132", "MEG 0133", "MEG 0131"] +def test_plot_topo_timechange(): + """Test that time change events are properly handled in plot_evoked_topo.""" + evoked = _get_epochs().average() + fig = plot_evoked_topo(evoked) + _fake_click(fig, fig.axes[0], (0.08, 0.65)) # open single channel + subfig = plt.gcf() + ax = plt.gca() + _fake_click(subfig, ax, (0.5, 0.5), kind="press") + init_time = ax._selectline.get_xdata()[0] + assert init_time == 0.0 + + # test existence of _current_time in main figure + assert hasattr(fig, "_current_time") + assert fig._current_time == 0.0 + + # test time change event from subfig and main fig + ui_events.publish(subfig, ui_events.TimeChange(time=0.1)) + assert ax._selectline.get_xdata()[0] == 0.1 + assert fig._current_time == 0.1 + ui_events.publish(subfig, ui_events.TimeChange(time=0.2)) + assert ax._selectline.get_xdata()[0] == 0.2 + assert fig._current_time == 0.2 + + plt.close("all") + + def test_plot_tfr_topo(): """Test plotting of TFR data.""" epochs = _get_epochs() From c57cff3b59dd930b7d3aa61d5ea0df7a12f49a3a Mon Sep 17 00:00:00 2001 From: Gnefil Date: Sun, 21 Jun 2026 22:46:34 +0800 Subject: [PATCH 06/18] ENH: add new functionality tutorial documentation --- tutorials/evoked/20_visualize_evoked.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tutorials/evoked/20_visualize_evoked.py b/tutorials/evoked/20_visualize_evoked.py index 3a91c38bb3d..3ef3708f7f8 100644 --- a/tutorials/evoked/20_visualize_evoked.py +++ b/tutorials/evoked/20_visualize_evoked.py @@ -261,6 +261,18 @@ def custom_func(x): mne.viz.plot_evoked_topo(evokeds_list) +# %% +# Topo and topomap figures have dynamic interactivity when plot for single +# :class:`~mne.Evoked` instance and allow time selection, which could be +# synchronized through link. + +single_evoked = evokeds_list[0] +fig_topomap = single_evoked.plot_topomap("interactive") +fig_topo = mne.viz.plot_evoked_topo(single_evoked) + +mne.viz.ui_events.link(fig_topomap, fig_topo) + + # %% # By default, :func:`~mne.viz.plot_evoked_topo` will plot all MEG sensors (if # present), so to get EEG sensors you would need to modify the evoked objects From 4e60c497c4f3976d1e7906bf1e0c5a104302e576 Mon Sep 17 00:00:00 2001 From: Gnefil Date: Sun, 21 Jun 2026 22:46:57 +0800 Subject: [PATCH 07/18] ENH: add to changelog --- doc/changes/dev/13968.newfeature.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 doc/changes/dev/13968.newfeature.rst diff --git a/doc/changes/dev/13968.newfeature.rst b/doc/changes/dev/13968.newfeature.rst new file mode 100644 index 00000000000..7b2bcaae793 --- /dev/null +++ b/doc/changes/dev/13968.newfeature.rst @@ -0,0 +1,2 @@ +Add `TimeChange` behaviour for :func:`mne.viz.plot_evoked_topo`, where time is marked with solid vertical after clicking on subfigure axis. +Add `merge` parameter to :func:`mne.viz.ui_events.link` that enables linking of groups which figs belong to, by `Lifeng Qiu Lin`_. \ No newline at end of file From a56f620e5285c580d371a71f3ce2fdb9426577ac Mon Sep 17 00:00:00 2001 From: Gnefil Date: Tue, 23 Jun 2026 14:03:34 +0800 Subject: [PATCH 08/18] ENH: solve review comments --- doc/changes/dev/13968.newfeature.rst | 4 ++-- mne/viz/topo.py | 11 +++++++---- tutorials/evoked/20_visualize_evoked.py | 4 ++-- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/doc/changes/dev/13968.newfeature.rst b/doc/changes/dev/13968.newfeature.rst index 7b2bcaae793..de986240c6e 100644 --- a/doc/changes/dev/13968.newfeature.rst +++ b/doc/changes/dev/13968.newfeature.rst @@ -1,2 +1,2 @@ -Add `TimeChange` behaviour for :func:`mne.viz.plot_evoked_topo`, where time is marked with solid vertical after clicking on subfigure axis. -Add `merge` parameter to :func:`mne.viz.ui_events.link` that enables linking of groups which figs belong to, by `Lifeng Qiu Lin`_. \ No newline at end of file +Add ``TimeChange`` behaviour for :func:`mne.viz.plot_evoked_topo`, where time is marked with solid vertical line after clicking on subfigure axis. +Add ``merge`` parameter to :func:`mne.viz.ui_events.link` that enables linking of groups which figs belong to, by `Lifeng Qiu Lin`_. \ No newline at end of file diff --git a/mne/viz/topo.py b/mne/viz/topo.py index 7cfde45f5f5..1164403ee4d 100644 --- a/mne/viz/topo.py +++ b/mne/viz/topo.py @@ -6,6 +6,7 @@ from copy import deepcopy from functools import partial +from inspect import signature import numpy as np from scipy import ndimage @@ -425,8 +426,12 @@ def _plot_topo_onpick(event, show_func): ax.set_facecolor(face_color) # allow custom function to override parameters - show_func(ax, ch_idx, orig_fig=fig) - plt_show(fig=subfig) + if "orig_fig" in signature(show_func).parameters: + show_func(ax, ch_idx, orig_fig=fig) + plt_show(fig=subfig) + else: + show_func(ax, ch_idx) + plt_show(fig=fig) except Exception as err: # matplotlib silently ignores exceptions in event handlers, @@ -647,7 +652,6 @@ def _rm_cursor(event): def _on_click(event): if event.inaxes == ax: publish(ax.figure, TimeChange(time=event.xdata)) - _update_selectline(event.xdata) def _on_time_change_sub(event): _update_selectline(event.time) @@ -664,7 +668,6 @@ def _update_selectline(time): face_brightness = np.dot(facecol, [299, 587, 114]) ax._cursorcolor = "white" if face_brightness < 150 else "black" - # setattr(orig_fig, "_current_time", None) ax._selectline = None ax._selectcolor = "white" if face_brightness < 150 else "black" diff --git a/tutorials/evoked/20_visualize_evoked.py b/tutorials/evoked/20_visualize_evoked.py index 3ef3708f7f8..c6839bfe4c3 100644 --- a/tutorials/evoked/20_visualize_evoked.py +++ b/tutorials/evoked/20_visualize_evoked.py @@ -262,9 +262,9 @@ def custom_func(x): mne.viz.plot_evoked_topo(evokeds_list) # %% -# Topo and topomap figures have dynamic interactivity when plot for single +# Topo and topomap figures have dynamic interactivity when plotting for single # :class:`~mne.Evoked` instance and allow time selection, which could be -# synchronized through link. +# synchronized through linking. single_evoked = evokeds_list[0] fig_topomap = single_evoked.plot_topomap("interactive") From 4c13c1a10f2e32b5a32baad96713134494b65c44 Mon Sep 17 00:00:00 2001 From: Lifeng <76589235+Gnefil@users.noreply.github.com> Date: Sun, 28 Jun 2026 18:35:18 +0800 Subject: [PATCH 09/18] Update tutorials/evoked/20_visualize_evoked.py Co-authored-by: Marijn van Vliet --- tutorials/evoked/20_visualize_evoked.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tutorials/evoked/20_visualize_evoked.py b/tutorials/evoked/20_visualize_evoked.py index c6839bfe4c3..3213db37aff 100644 --- a/tutorials/evoked/20_visualize_evoked.py +++ b/tutorials/evoked/20_visualize_evoked.py @@ -282,7 +282,10 @@ def custom_func(x): # # In interactive sessions, both approaches to topographical plotting allow # you to click one of the sensor subplots to open a larger version of the -# evoked plot at that sensor. +# evoked plot at that sensor. In this view, you can select a time by clicking +# somewhere in the timecourse. The selected time is marked with a vertical line on +# all channels, also in the original figure and any other single-channel figures +# that are open. # # # 3D Field Maps From 60def6c651a71b4669a0fec6a8e6dab68d83e601 Mon Sep 17 00:00:00 2001 From: Gnefil Date: Sun, 28 Jun 2026 18:40:14 +0800 Subject: [PATCH 10/18] ENH: solve second round review comments --- mne/viz/topo.py | 9 ++++----- tutorials/evoked/20_visualize_evoked.py | 12 ------------ 2 files changed, 4 insertions(+), 17 deletions(-) diff --git a/mne/viz/topo.py b/mne/viz/topo.py index 1164403ee4d..13ee9e68887 100644 --- a/mne/viz/topo.py +++ b/mne/viz/topo.py @@ -1183,6 +1183,10 @@ def _plot_evoked_topo( setattr(fig, "_current_time", None) + def on_time_change(event, fig): + """Respond to a time change UI event.""" + fig._current_time = event.time + subscribe(fig, "time_change", partial(on_time_change, fig=fig)) add_background_image(fig, fig_background) @@ -1218,11 +1222,6 @@ def _plot_evoked_topo( return fig -def on_time_change(event, fig): - """Respond to a time change UI event.""" - fig._current_time = event.time - - def _plot_update_evoked_topo_proj(params, bools): """Update topo sensor plots.""" evokeds = [e.copy() for e in params["evokeds"]] diff --git a/tutorials/evoked/20_visualize_evoked.py b/tutorials/evoked/20_visualize_evoked.py index 3213db37aff..5e5a581a718 100644 --- a/tutorials/evoked/20_visualize_evoked.py +++ b/tutorials/evoked/20_visualize_evoked.py @@ -261,18 +261,6 @@ def custom_func(x): mne.viz.plot_evoked_topo(evokeds_list) -# %% -# Topo and topomap figures have dynamic interactivity when plotting for single -# :class:`~mne.Evoked` instance and allow time selection, which could be -# synchronized through linking. - -single_evoked = evokeds_list[0] -fig_topomap = single_evoked.plot_topomap("interactive") -fig_topo = mne.viz.plot_evoked_topo(single_evoked) - -mne.viz.ui_events.link(fig_topomap, fig_topo) - - # %% # By default, :func:`~mne.viz.plot_evoked_topo` will plot all MEG sensors (if # present), so to get EEG sensors you would need to modify the evoked objects From bfbdf83289cedfe6857740540b0f3d39dec19d50 Mon Sep 17 00:00:00 2001 From: Gnefil Date: Mon, 29 Jun 2026 14:30:15 +0800 Subject: [PATCH 11/18] ENH: add note in evoked topo for TimeChange event --- mne/viz/evoked.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/mne/viz/evoked.py b/mne/viz/evoked.py index a62d2379f03..d953e4bf3ea 100644 --- a/mne/viz/evoked.py +++ b/mne/viz/evoked.py @@ -1243,6 +1243,13 @@ def plot_evoked_topo( ------- fig : instance of matplotlib.figure.Figure Images of evoked responses at sensor locations. + + Notes + ----- + The figure will publish and subscribe to the following UI events: + + * :class:`~mne.viz.ui_events.TimeChange` + """ if type(evoked) not in (tuple, list): evoked = [evoked] From 8a2d953b9aef1e84fefde20b1287adaddf5101d1 Mon Sep 17 00:00:00 2001 From: Gnefil Date: Mon, 29 Jun 2026 15:34:39 +0800 Subject: [PATCH 12/18] ENH: change the position of notes to avoid duplicate --- mne/evoked.py | 4 ++++ mne/viz/evoked.py | 6 ------ 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/mne/evoked.py b/mne/evoked.py index 17048bf0193..b481782b529 100644 --- a/mne/evoked.py +++ b/mne/evoked.py @@ -618,6 +618,10 @@ def plot_topo( Notes ----- + The figure will publish and subscribe to the following UI events: + + * :class:`~mne.viz.ui_events.TimeChange` + .. versionadded:: 0.10.0 """ return plot_evoked_topo( diff --git a/mne/viz/evoked.py b/mne/viz/evoked.py index d953e4bf3ea..a4e8b9c380e 100644 --- a/mne/viz/evoked.py +++ b/mne/viz/evoked.py @@ -1244,12 +1244,6 @@ def plot_evoked_topo( fig : instance of matplotlib.figure.Figure Images of evoked responses at sensor locations. - Notes - ----- - The figure will publish and subscribe to the following UI events: - - * :class:`~mne.viz.ui_events.TimeChange` - """ if type(evoked) not in (tuple, list): evoked = [evoked] From cd01d320cc9e10b0db5d2ffd072a41abef1f1c1d Mon Sep 17 00:00:00 2001 From: Gnefil Date: Mon, 29 Jun 2026 17:56:57 +0800 Subject: [PATCH 13/18] ENH: remove trailing blank line --- mne/viz/evoked.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mne/viz/evoked.py b/mne/viz/evoked.py index a4e8b9c380e..a62d2379f03 100644 --- a/mne/viz/evoked.py +++ b/mne/viz/evoked.py @@ -1243,7 +1243,6 @@ def plot_evoked_topo( ------- fig : instance of matplotlib.figure.Figure Images of evoked responses at sensor locations. - """ if type(evoked) not in (tuple, list): evoked = [evoked] From c84e16a8c199d30e9a832a37f720e8a26d6f3423 Mon Sep 17 00:00:00 2001 From: Gnefil Date: Tue, 30 Jun 2026 17:07:12 +0800 Subject: [PATCH 14/18] ENH: update link signature, plot_evoked_topo docstring, and add API change log --- doc/changes/dev/13968.apichange.rst | 1 + mne/evoked.py | 10 ---------- mne/viz/evoked.py | 7 +++++++ mne/viz/topo.py | 4 ++-- mne/viz/ui_events.py | 10 ++++++---- 5 files changed, 16 insertions(+), 16 deletions(-) create mode 100644 doc/changes/dev/13968.apichange.rst diff --git a/doc/changes/dev/13968.apichange.rst b/doc/changes/dev/13968.apichange.rst new file mode 100644 index 00000000000..eea390cd7d0 --- /dev/null +++ b/doc/changes/dev/13968.apichange.rst @@ -0,0 +1 @@ +Now ``on_pick`` callable parameter accepts ``orig_fig`` in :func:`mne.viz.iter_topography`, to preserve backward compatibility it is made optional, by `Lifeng Qiu Lin`_. diff --git a/mne/evoked.py b/mne/evoked.py index b481782b529..7e8fba9f86c 100644 --- a/mne/evoked.py +++ b/mne/evoked.py @@ -614,16 +614,6 @@ def plot_topo( select=False, show=True, ): - """. - - Notes - ----- - The figure will publish and subscribe to the following UI events: - - * :class:`~mne.viz.ui_events.TimeChange` - - .. versionadded:: 0.10.0 - """ return plot_evoked_topo( self, layout=layout, diff --git a/mne/viz/evoked.py b/mne/viz/evoked.py index a62d2379f03..b95b9e6d695 100644 --- a/mne/viz/evoked.py +++ b/mne/viz/evoked.py @@ -1243,6 +1243,13 @@ def plot_evoked_topo( ------- fig : instance of matplotlib.figure.Figure Images of evoked responses at sensor locations. + + Notes + ----- + The figure will publish and subscribe to the following UI events: + + * :class:`~mne.viz.ui_events.TimeChange` + .. versionadded:: 1.13.0 """ if type(evoked) not in (tuple, list): evoked = [evoked] diff --git a/mne/viz/topo.py b/mne/viz/topo.py index 13ee9e68887..d7c35bb6128 100644 --- a/mne/viz/topo.py +++ b/mne/viz/topo.py @@ -64,7 +64,7 @@ def iter_topography( on_pick : callable | None The callback function to be invoked on clicking one of the axes. Is supposed to instantiate the following - API: ``function(axis, channel_index)``. + API: ``function(axis, channel_index, orig_fig)``. fig : matplotlib.figure.Figure | None The figure object to be considered. If None, a new figure will be created. @@ -677,7 +677,7 @@ def _update_selectline(time): subscribe(ax.figure, "time_change", _on_time_change_sub) - link(orig_fig, ax.figure, merge=True) + link(orig_fig, ax.figure, recursive=True) ymin, ymax = ax.get_ylim() # don't pass vline or hline here (this fxn doesn't do hvline_color): diff --git a/mne/viz/ui_events.py b/mne/viz/ui_events.py index 6b0986a7076..302d6b7e8b3 100644 --- a/mne/viz/ui_events.py +++ b/mne/viz/ui_events.py @@ -407,7 +407,9 @@ def unsubscribe(fig, event_names, callback=None, *, verbose=None): @verbose -def link(*figs, include_events=None, exclude_events=None, merge=False, verbose=None): +def link( + *figs, include_events=None, exclude_events=None, recursive=False, verbose=None +): """Link the event channels of two figures together. When event channels are linked, any events that are published on one @@ -426,7 +428,7 @@ def link(*figs, include_events=None, exclude_events=None, merge=False, verbose=N exclude_events : list of str | None Select which events not to publish across figures. By default (``None``), no events are excluded. - merge : bool + recursive : bool If ``True``, also link the existing link-groups that figs already belong to, so all members are mutually linked. %(verbose)s @@ -442,8 +444,8 @@ def link(*figs, include_events=None, exclude_events=None, merge=False, verbose=N if fig not in _event_channel_links: _event_channel_links[fig] = weakref.WeakKeyDictionary() - # Merge link groups that the figures already belong - if merge: + # Recursive link groups that the figures already belong + if recursive: figs_set = weakref.WeakSet() for fig in figs: figs_set.add(fig) From 1fb402e8bdda7bc2608d43350ee0bb477dcc50a9 Mon Sep 17 00:00:00 2001 From: Gnefil Date: Wed, 1 Jul 2026 10:20:53 +0800 Subject: [PATCH 15/18] ENH: correct indentation in versiontag --- 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 b95b9e6d695..a1d685dac3d 100644 --- a/mne/viz/evoked.py +++ b/mne/viz/evoked.py @@ -1249,7 +1249,7 @@ def plot_evoked_topo( The figure will publish and subscribe to the following UI events: * :class:`~mne.viz.ui_events.TimeChange` - .. versionadded:: 1.13.0 + .. versionadded:: 1.13.0 """ if type(evoked) not in (tuple, list): evoked = [evoked] From 746dbbcfd1726ca61773f8780e1e0c8bc418b27d Mon Sep 17 00:00:00 2001 From: Gnefil Date: Wed, 1 Jul 2026 12:25:16 +0800 Subject: [PATCH 16/18] ENH: add black space before versiontag --- mne/viz/evoked.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mne/viz/evoked.py b/mne/viz/evoked.py index a1d685dac3d..75867294b53 100644 --- a/mne/viz/evoked.py +++ b/mne/viz/evoked.py @@ -1249,7 +1249,8 @@ def plot_evoked_topo( The figure will publish and subscribe to the following UI events: * :class:`~mne.viz.ui_events.TimeChange` - .. versionadded:: 1.13.0 + + .. versionadded:: 1.13.0 """ if type(evoked) not in (tuple, list): evoked = [evoked] From 4e9ed06c946d6bc43000602917b03f6ec229e4c2 Mon Sep 17 00:00:00 2001 From: Lifeng <76589235+Gnefil@users.noreply.github.com> Date: Wed, 1 Jul 2026 19:54:46 +0800 Subject: [PATCH 17/18] Apply suggestions from code review Co-authored-by: Marijn van Vliet --- doc/changes/dev/13968.apichange.rst | 2 +- doc/changes/dev/13968.newfeature.rst | 2 +- mne/viz/ui_events.py | 7 ++++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/doc/changes/dev/13968.apichange.rst b/doc/changes/dev/13968.apichange.rst index eea390cd7d0..fbc12493536 100644 --- a/doc/changes/dev/13968.apichange.rst +++ b/doc/changes/dev/13968.apichange.rst @@ -1 +1 @@ -Now ``on_pick`` callable parameter accepts ``orig_fig`` in :func:`mne.viz.iter_topography`, to preserve backward compatibility it is made optional, by `Lifeng Qiu Lin`_. +In :func:`mne.viz.iter_topography`, the ``on_pick`` callable for sub-figures now has a new parameter ``orig_fig`` that refers to the main figure. To preserve backward compatibility, it is made optional. By `Lifeng Qiu Lin`_. diff --git a/doc/changes/dev/13968.newfeature.rst b/doc/changes/dev/13968.newfeature.rst index de986240c6e..106eef9ac4d 100644 --- a/doc/changes/dev/13968.newfeature.rst +++ b/doc/changes/dev/13968.newfeature.rst @@ -1,2 +1,2 @@ Add ``TimeChange`` behaviour for :func:`mne.viz.plot_evoked_topo`, where time is marked with solid vertical line after clicking on subfigure axis. -Add ``merge`` parameter to :func:`mne.viz.ui_events.link` that enables linking of groups which figs belong to, by `Lifeng Qiu Lin`_. \ No newline at end of file +Add ``recursive`` parameter to :func:`mne.viz.ui_events.link` that enables linking to a target figure, as well as all figures that the target figure links to, by `Lifeng Qiu Lin`_. \ No newline at end of file diff --git a/mne/viz/ui_events.py b/mne/viz/ui_events.py index 302d6b7e8b3..d66c6b611b1 100644 --- a/mne/viz/ui_events.py +++ b/mne/viz/ui_events.py @@ -444,16 +444,17 @@ def link( if fig not in _event_channel_links: _event_channel_links[fig] = weakref.WeakKeyDictionary() - # Recursive link groups that the figures already belong if recursive: + # Also link to anything that is linked to any of the figures in `figs`. figs_set = weakref.WeakSet() for fig in figs: figs_set.add(fig) for linked_fig in _event_channel_links[fig].keys(): figs_set.add(linked_fig) - # Deliberately let current include and exclude events dominate over past ones + # Deliberately let current include and exclude events dominate over past ones. figs = figs_set - # Link the event channels plainly + + # Do the actual linking. for fig1 in figs: for fig2 in figs: if fig1 is not fig2: From d97e4d7bf87d377cb00584a392b99c9221361b67 Mon Sep 17 00:00:00 2001 From: Marijn van Vliet Date: Fri, 3 Jul 2026 15:15:28 +0300 Subject: [PATCH 18/18] Fix unit test --- mne/viz/tests/test_ui_events.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mne/viz/tests/test_ui_events.py b/mne/viz/tests/test_ui_events.py index 5b99aebacaa..9fac7041999 100644 --- a/mne/viz/tests/test_ui_events.py +++ b/mne/viz/tests/test_ui_events.py @@ -234,10 +234,10 @@ def callback(event): ui_events.publish(fig2, ui_events.TimeChange(time=10.2)) assert len(callback_calls) == 2 # Only called for both figures once - # Test merge link groups + # Test recursive linking. fig3 = plt.figure() ui_events.subscribe(fig3, "time_change", callback) - ui_events.link(fig2, fig3, merge=True) # fig1 and fig2 are already linked + ui_events.link(fig2, fig3, recursive=True) # fig1 and fig2 are already linked callback_calls.clear() ui_events.publish(fig3, ui_events.TimeChange(time=10.2)) ui_events.publish(fig1, ui_events.TimeChange(time=10.2))