Skip to content

Commit cedefa0

Browse files
ruuskasSanteri Ruuskanenwmvanvliet
authored
Add UI Event linking to DraggableColorbar (mne-tools#12057)
Co-authored-by: Santeri Ruuskanen <santeri.ruuskanen@aalto.fi> Co-authored-by: Marijn van Vliet <w.m.vanvliet@gmail.com>
1 parent 81b7ddf commit cedefa0

File tree

8 files changed

+189
-16
lines changed

8 files changed

+189
-16
lines changed

doc/changes/devel.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ Version 1.6.dev0 (development)
2323

2424
Enhancements
2525
~~~~~~~~~~~~
26-
- Improve tests for saving splits with `Epochs` (:gh:`11884` by `Dmitrii Altukhov`_)
26+
- Improve tests for saving splits with :class:`mne.Epochs` (:gh:`11884` by `Dmitrii Altukhov`_)
2727
- Added functionality for linking interactive figures together, such that changing one figure will affect another, see :ref:`tut-ui-events` and :mod:`mne.viz.ui_events`. Current figures implementing UI events are :func:`mne.viz.plot_topomap` and :func:`mne.viz.plot_source_estimates` (:gh:`11685` :gh:`11891` by `Marijn van Vliet`_)
2828
- HTML anchors for :class:`mne.Report` now reflect the ``section-title`` of the report items rather than using a global incrementor ``global-N`` (:gh:`11890` by `Eric Larson`_)
2929
- Added public :func:`mne.io.write_info` to complement :func:`mne.io.read_info` (:gh:`11918` by `Eric Larson`_)
@@ -37,6 +37,7 @@ Enhancements
3737
- Add support for writing forward solutions to HDF5 and convenience function :meth:`mne.Forward.save` (:gh:`12036` by `Eric Larson`_)
3838
- Refactored internals of :func:`mne.read_annotations` (:gh:`11964` by `Paul Roujansky`_)
3939
- Enhance :func:`~mne.viz.plot_evoked_field` with a GUI that has controls for time, colormap, and contour lines (:gh:`11942` by `Marijn van Vliet`_)
40+
- Add :class:`mne.viz.ui_events.UIEvent` linking for interactive colorbars, allowing users to link figures and change the colormap and limits interactively. This supports :func:`~mne.viz.plot_evoked_topomap`, :func:`~mne.viz.plot_ica_components`, :func:`~mne.viz.plot_tfr_topomap`, :func:`~mne.viz.plot_projs_topomap`, :meth:`~mne.Evoked.plot_image`, and :meth:`~mne.Epochs.plot_image` (:gh:`12057` by `Santeri Ruuskanen`_)
4041

4142
Bugs
4243
~~~~

mne/viz/epochs.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -661,7 +661,9 @@ def _plot_epochs_image(
661661
this_colorbar = cbar(im, cax=ax["colorbar"])
662662
this_colorbar.ax.set_ylabel(unit, rotation=270, labelpad=12)
663663
if cmap[1]:
664-
ax_im.CB = DraggableColorbar(this_colorbar, im)
664+
ax_im.CB = DraggableColorbar(
665+
this_colorbar, im, kind="epochs_image", ch_type=unit
666+
)
665667
with warnings.catch_warnings(record=True):
666668
warnings.simplefilter("ignore")
667669
tight_layout(fig=fig)

mne/viz/evoked.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -958,7 +958,7 @@ def _plot_image(
958958
cbar = plt.colorbar(im, ax=ax)
959959
cbar.ax.set_title(ch_unit)
960960
if cmap[1]:
961-
ax.CB = DraggableColorbar(cbar, im)
961+
ax.CB = DraggableColorbar(cbar, im, "evoked_image", this_type)
962962

963963
ylabel = "Channels" if show_names else "Channel (index)"
964964
t = titles[this_type] + " (%d channel%s" % (len(data), _pl(data)) + t_end

mne/viz/tests/test_utils.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
from mne.viz.utils import (
1313
compare_fiff,
1414
_fake_click,
15+
_fake_keypress,
16+
_fake_scroll,
1517
_compute_scalings,
1618
_validate_if_list_of_axes,
1719
_get_color_list,
@@ -20,15 +22,18 @@
2022
_make_event_color_dict,
2123
concatenate_images,
2224
)
25+
from mne.viz.ui_events import link, subscribe, ColormapRange
2326
from mne.viz import ClickableImage, add_background_image, mne_analyze_colormap
2427
from mne.io import read_raw_fif
2528
from mne.event import read_events
2629
from mne.epochs import Epochs
30+
from mne import read_evokeds
2731

2832
base_dir = Path(__file__).parent.parent.parent / "io" / "tests" / "data"
2933
raw_fname = base_dir / "test_raw.fif"
3034
cov_fname = base_dir / "test-cov.fif"
3135
ev_fname = base_dir / "test_raw-eve.fif"
36+
ave_fname = base_dir / "test-ave.fif"
3237

3338

3439
def test_setup_vmin_vmax_warns():
@@ -202,3 +207,71 @@ def test_concatenate_images(a_w, a_h, b_w, b_h, axis):
202207
else:
203208
want_shape = (max(a_h, b_h), a_w + b_w, 3)
204209
assert img.shape == want_shape
210+
211+
212+
def test_draggable_colorbar():
213+
"""Test that DraggableColorbar publishes correct UI Events."""
214+
evokeds = read_evokeds(ave_fname)
215+
left_auditory = evokeds[0]
216+
right_auditory = evokeds[1]
217+
vmin, vmax = -400, 400
218+
fig = left_auditory.plot_topomap("interactive", vlim=(vmin, vmax))
219+
fig2 = right_auditory.plot_topomap("interactive", vlim=(vmin, vmax))
220+
link(fig, fig2)
221+
callback_calls = []
222+
223+
def callback(event):
224+
callback_calls.append(event)
225+
226+
subscribe(fig, "colormap_range", callback)
227+
228+
# Test that correct event is published
229+
_fake_keypress(fig, "down")
230+
_fake_keypress(fig, "up")
231+
assert len(callback_calls) == 2
232+
event = callback_calls.pop()
233+
assert type(event) is ColormapRange
234+
# Test that scrolling changes color limits
235+
_fake_scroll(fig, 10, 10, 1)
236+
event = callback_calls.pop()
237+
assert abs(event.fmin) < abs(vmin)
238+
assert abs(event.fmax) < abs(vmax)
239+
fmin, fmax = event.fmin, event.fmax
240+
_fake_scroll(fig, 10, 10, -1)
241+
event = callback_calls.pop()
242+
assert abs(event.fmin) > abs(fmin)
243+
assert abs(event.fmax) > abs(fmax)
244+
fmin, fmax = event.fmin, event.fmax
245+
# Test that plus and minus change color limits
246+
_fake_keypress(fig, "+")
247+
event = callback_calls.pop()
248+
assert abs(event.fmin) < abs(fmin)
249+
assert abs(event.fmax) < abs(fmax)
250+
fmin, fmax = event.fmin, event.fmax
251+
_fake_keypress(fig, "-")
252+
event = callback_calls.pop()
253+
assert abs(event.fmin) > abs(fmin)
254+
assert abs(event.fmax) > abs(fmax)
255+
fmin, fmax = event.fmin, event.fmax
256+
# Test that page up and page down change color limits
257+
_fake_keypress(fig, "pageup")
258+
event = callback_calls.pop()
259+
assert event.fmin < fmin
260+
assert event.fmax < fmax
261+
fmin, fmax = event.fmin, event.fmax
262+
_fake_keypress(fig, "pagedown")
263+
event = callback_calls.pop()
264+
assert event.fmin > fmin
265+
assert event.fmax > fmax
266+
# Test that space key resets color limits
267+
_fake_keypress(fig, " ")
268+
event = callback_calls.pop()
269+
assert event.fmax == vmax
270+
assert event.fmin == vmin
271+
# Test that colormap change in one figure changes that of another one
272+
cmap_want = fig.axes[0].CB.cycle[fig.axes[0].CB.index + 1]
273+
cmap_old = fig.axes[0].CB.mappable.get_cmap().name
274+
_fake_keypress(fig, "down")
275+
cmap_new1 = fig.axes[0].CB.mappable.get_cmap().name
276+
cmap_new2 = fig2.axes[0].CB.mappable.get_cmap().name
277+
assert cmap_new1 == cmap_new2 == cmap_want != cmap_old

mne/viz/topo.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -457,7 +457,7 @@ def _imshow_tfr(
457457
else:
458458
cbar = plt.colorbar(mappable=img, ax=ax)
459459
if interactive_cmap:
460-
ax.CB = DraggableColorbar(cbar, img)
460+
ax.CB = DraggableColorbar(cbar, img, kind="tfr_image", ch_type=None)
461461
ax.RS = RectangleSelector(ax, onselect=onselect) # reference must be kept
462462

463463
return t_end

mne/viz/topomap.py

Lines changed: 62 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,16 @@ def _plot_update_evoked_topomap(params, bools):
298298

299299

300300
def _add_colorbar(
301-
ax, im, cmap, side="right", pad=0.05, title=None, format=None, size="5%"
301+
ax,
302+
im,
303+
cmap,
304+
side="right",
305+
pad=0.05,
306+
title=None,
307+
format=None,
308+
size="5%",
309+
kind=None,
310+
ch_type=None,
302311
):
303312
"""Add a colorbar to an axis."""
304313
import matplotlib.pyplot as plt
@@ -308,7 +317,7 @@ def _add_colorbar(
308317
cax = divider.append_axes(side, size=size, pad=pad)
309318
cbar = plt.colorbar(im, cax=cax, format=format)
310319
if cmap is not None and cmap[1]:
311-
ax.CB = DraggableColorbar(cbar, im)
320+
ax.CB = DraggableColorbar(cbar, im, kind, ch_type)
312321
if title is not None:
313322
cax.set_title(title, y=1.05, fontsize=10)
314323
return cbar, cax
@@ -587,7 +596,15 @@ def _plot_projs_topomap(
587596
)
588597

589598
if colorbar:
590-
_add_colorbar(ax, im, cmap, title=units, format=cbar_fmt)
599+
_add_colorbar(
600+
ax,
601+
im,
602+
cmap,
603+
title=units,
604+
format=cbar_fmt,
605+
kind="projs_topomap",
606+
ch_type=_ch_type,
607+
)
591608

592609
return ax.get_figure()
593610

@@ -973,7 +990,7 @@ def plot_topomap(
973990
.. versionadded:: 0.20
974991
%(res_topomap)s
975992
%(size_topomap)s
976-
%(cmap_topomap_simple)s
993+
%(cmap_topomap)s
977994
%(vlim_plot_topomap)s
978995
979996
.. versionadded:: 1.2
@@ -1454,7 +1471,16 @@ def _plot_ica_topomap(
14541471
ch_type=ch_type,
14551472
)[0]
14561473
if colorbar:
1457-
cbar, cax = _add_colorbar(axes, im, cmap, pad=0.05, title="AU", format="%3.2f")
1474+
cbar, cax = _add_colorbar(
1475+
axes,
1476+
im,
1477+
cmap,
1478+
pad=0.05,
1479+
title="AU",
1480+
format="%3.2f",
1481+
kind="ica_topomap",
1482+
ch_type=ch_type,
1483+
)
14581484
cbar.ax.tick_params(labelsize=12)
14591485
cbar.set_ticks(vlim)
14601486
_hide_frame(axes)
@@ -1685,7 +1711,15 @@ def plot_ica_components(
16851711
im.axes.set_label(ica._ica_names[ii])
16861712
if colorbar:
16871713
cbar, cax = _add_colorbar(
1688-
ax, im, cmap, title="AU", side="right", pad=0.05, format=cbar_fmt
1714+
ax,
1715+
im,
1716+
cmap,
1717+
title="AU",
1718+
side="right",
1719+
pad=0.05,
1720+
format=cbar_fmt,
1721+
kind="ica_comp_topomap",
1722+
ch_type=ch_type,
16891723
)
16901724
cbar.ax.tick_params(labelsize=12)
16911725
cbar.set_ticks(_vlim)
@@ -1956,7 +1990,15 @@ def plot_tfr_topomap(
19561990
from matplotlib import ticker
19571991

19581992
units = _handle_default("units", units)["misc"]
1959-
cbar, cax = _add_colorbar(axes, im, cmap, title=units, format=cbar_fmt)
1993+
cbar, cax = _add_colorbar(
1994+
axes,
1995+
im,
1996+
cmap,
1997+
title=units,
1998+
format=cbar_fmt,
1999+
kind="tfr_topomap",
2000+
ch_type=ch_type,
2001+
)
19602002
if locator is None:
19612003
locator = ticker.MaxNLocator(nbins=5)
19622004
cbar.locator = locator
@@ -2363,6 +2405,11 @@ def _slider_changed(val):
23632405
kwargs=kwargs,
23642406
),
23652407
)
2408+
subscribe(
2409+
fig,
2410+
"colormap_range",
2411+
partial(_on_colormap_range, kwargs=kwargs),
2412+
)
23662413

23672414
if colorbar:
23682415
if interactive:
@@ -2383,7 +2430,9 @@ def _slider_changed(val):
23832430
cbar.ax.tick_params(labelsize=7)
23842431
if cmap[1]:
23852432
for im in images:
2386-
im.axes.CB = DraggableColorbar(cbar, im)
2433+
im.axes.CB = DraggableColorbar(
2434+
cbar, im, kind="evoked_topomap", ch_type=ch_type
2435+
)
23872436

23882437
if proj == "interactive":
23892438
_check_delayed_ssp(evoked)
@@ -2460,6 +2509,11 @@ def _on_time_change(
24602509
ax.figure.canvas.draw_idle()
24612510

24622511

2512+
def _on_colormap_range(event, kwargs):
2513+
"""Handle updating colormap range."""
2514+
kwargs.update(vlim=(event.fmin, event.fmax), cmap=event.cmap)
2515+
2516+
24632517
def _plot_topomap_multi_cbar(
24642518
data,
24652519
pos,

mne/viz/ui_events.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@
1111
"""
1212
import contextlib
1313
from dataclasses import dataclass
14-
from typing import Optional, List
14+
from typing import Optional, List, Union
1515
import weakref
1616
import re
1717

18+
from matplotlib.colors import Colormap
19+
1820
from ..utils import warn, fill_doc, _validate_type, logger, verbose
1921

2022
# Global dict {fig: channel} containing all currently active event channels.
@@ -114,26 +116,38 @@ class ColormapRange(UIEvent):
114116
kind : str
115117
Kind of colormap being updated. The Notes section of the drawing
116118
routine publishing this event should mention the possible kinds.
119+
ch_type : str
120+
Type of sensor the data originates from.
117121
%(fmin_fmid_fmax)s
118122
%(alpha)s
123+
cmap : str
124+
The colormap to use. Either string or matplotlib.colors.Colormap
125+
instance.
119126
120127
Attributes
121128
----------
122129
kind : str
123130
Kind of colormap being updated. The Notes section of the drawing
124131
routine publishing this event should mention the possible kinds.
132+
ch_type : str
133+
Type of sensor the data originates from.
125134
unit : str
126135
The unit of the values.
127136
%(ui_event_name_source)s
128137
%(fmin_fmid_fmax)s
129138
%(alpha)s
139+
cmap : str
140+
The colormap to use. Either string or matplotlib.colors.Colormap
141+
instance.
130142
"""
131143

132144
kind: str
145+
ch_type: Optional[str] = None
133146
fmin: Optional[float] = None
134147
fmid: Optional[float] = None
135148
fmax: Optional[float] = None
136149
alpha: Optional[bool] = None
150+
cmap: Optional[Union[Colormap, str]] = None
137151

138152

139153
@dataclass

0 commit comments

Comments
 (0)