Repository: mne-tools/mne-python

Issue:
plot_topomap()'s cnorm param should accept a CenteredNorm
Plotting T statistics on topographic maps with @SophieHerbst, I discovered that `mne.viz.plot_topomap()` only accepts `TwoSlopeNorm` as `cnorm` paramter.

However, I believe for our purpose, [`TwoSlopeNorm`](https://matplotlib.org/stable/api/_as_gen/matplotlib.colors.TwoSlopeNorm.html) is not the best solution:

> Normalize data with a set center.
> Useful when mapping data with an unequal rates of change around a conceptual center, e.g., data that range from -2 to 4, with 0 as the midpoint.

Instead, I believe [`CenteredNorm`](https://matplotlib.org/stable/api/_as_gen/matplotlib.colors.CenteredNorm.html) makes much more sense:

> Normalize symmetrical data around a center (0 by default).
> Unlike [TwoSlopeNorm](https://matplotlib.org/stable/api/_as_gen/matplotlib.colors.TwoSlopeNorm.html#matplotlib.colors.TwoSlopeNorm), [CenteredNorm](https://matplotlib.org/stable/api/_as_gen/matplotlib.colors.CenteredNorm.html#matplotlib.colors.CenteredNorm) applies an equal rate of change around the center.
> Useful when mapping symmetrical data around a conceptual center e.g., data that range from -2 to 4, with 0 as the midpoint, and with equal rates of change around that midpoint.

Hence, I'd like to have us accept `CenteredNorm` as well. Currently, this is not possible:

https://github.com/mne-tools/mne-python/blob/0452d69396ade5186bfad166aa7c7f608b288867/mne/viz/topomap.py#L800




Discussion:
probably `TwoSlopeNorm` and `CenteredNorm` have a parent class they both inherit from; if so we could/should use that in the call to `_validate_type`
Yes they do. Although I almost believe that there are probably very few (no?) use cases for TwoSlopeNorm when plotting topomaps... because it creates "asymmetrical" color gradients 🤔
Why is `TwoSlopeNorm` the default here instead of `CenteredNorm`?
ping @cbrnr :)
To support asymmetrical mappings and still have white represent zero. For example, ERD/ERS values range from -1 to e.g. 2.5, but I want 0 to be "in the middle" (white).
I don't understand what they mean with "equal/unequal rates of change around a conceptual center" though.
Say the data is from -1 to 2.5, and the colormap is blue --> white ---> red. We would normally want the colormap to be symmetric: -2.5 to be dark blue, 0 to be white and 2.5 to be dark red. I think the `TwoSlopeNorm` will make -1 dark blue, so an assymetric colormap.
Aah I see. So, if you just want a symmetric colormap like in my example, normally you would just use `vmin = -vmax` and be done. But in the exceptional case where you don't want a symmetric map, this used to be impossible without the `cnorm` parameter. Hence, the doc for `cnorm` mentions `TwoSlopeNorm` because it describes this case.

Resolving patch:
diff --git a/mne/viz/topomap.py b/mne/viz/topomap.py
--- a/mne/viz/topomap.py
+++ b/mne/viz/topomap.py
@@ -28,7 +28,7 @@
                        _MEG_CH_TYPES_SPLIT)
 from ..utils import (_clean_names, _time_mask, verbose, logger, fill_doc,
                      _validate_type, _check_sphere, _check_option, _is_numeric,
-                     warn, check_version)
+                     warn)
 from .utils import (tight_layout, _setup_vmin_vmax, _prepare_trellis,
                     _check_delayed_ssp, _draw_proj_checkbox, figure_nobar,
                     plt_show, _process_times, DraggableColorbar,
@@ -793,11 +793,8 @@ def plot_topomap(data, pos, vmin=None, vmax=None, cmap=None, sensors=True,
     for more details on colormap normalization.
     """
     sphere = _check_sphere(sphere)
-    if check_version("matplotlib", "3.2.0"):
-        from matplotlib.colors import TwoSlopeNorm
-    else:
-        from matplotlib.colors import DivergingNorm as TwoSlopeNorm
-    _validate_type(cnorm, (TwoSlopeNorm, None), 'cnorm')
+    from matplotlib.colors import Normalize
+    _validate_type(cnorm, (Normalize, None), 'cnorm')
     if cnorm is not None:
         if vmin is not None:
             warn(f"vmin={cnorm.vmin} is implicitly defined by cnorm, ignoring "


Test patch:
diff --git a/mne/viz/tests/test_topomap.py b/mne/viz/tests/test_topomap.py
--- a/mne/viz/tests/test_topomap.py
+++ b/mne/viz/tests/test_topomap.py
@@ -687,6 +687,7 @@ def test_plot_topomap_cnorm():
         from matplotlib.colors import TwoSlopeNorm
     else:
         from matplotlib.colors import DivergingNorm as TwoSlopeNorm
+    from matplotlib.colors import PowerNorm
 
     rng = np.random.default_rng(42)
     v = rng.uniform(low=-1, high=2.5, size=64)
@@ -709,6 +710,9 @@ def test_plot_topomap_cnorm():
     with pytest.warns(RuntimeWarning, match=msg):
         plot_topomap(v, info, vmax=10, cnorm=cnorm)
 
+    # try another subclass of mpl.colors.Normalize
+    plot_topomap(v, info, cnorm=PowerNorm(0.5))
+
 
 def test_plot_bridged_electrodes():
     """Test plotting of bridged electrodes."""